Skip to content

Commit a381ba7

Browse files
authored
Merge pull request #7 from Kocal/MethodsShouldBePublicOrPrivateRule
2 parents 45580c1 + 41ea205 commit a381ba7

File tree

11 files changed

+288
-1
lines changed

11 files changed

+288
-1
lines changed

README.md

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ abstract class BaseComponent
256256
}
257257

258258
#[AsTwigComponent]
259-
final class Alert extends BaseComponent
259+
class Alert extends BaseComponent
260260
{
261261
}
262262
```
@@ -300,6 +300,65 @@ final class Alert
300300

301301
<br>
302302

303+
### MethodsShouldBePublicOrPrivateRule
304+
305+
Enforces that all methods in Twig Components are either public or private, but not protected.
306+
Since Twig Components must be final classes and inheritance is forbidden (see `ForbiddenInheritanceRule`), protected methods serve no purpose and should be avoided.
307+
308+
```yaml
309+
rules:
310+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule
311+
```
312+
313+
```php
314+
// src/Twig/Components/Alert.php
315+
namespace App\Twig\Components;
316+
317+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
318+
319+
#[AsTwigComponent]
320+
final class Alert
321+
{
322+
public string $message;
323+
324+
protected function formatMessage(): string
325+
{
326+
return strtoupper($this->message);
327+
}
328+
}
329+
```
330+
331+
:x:
332+
333+
<br>
334+
335+
```php
336+
// src/Twig/Components/Alert.php
337+
namespace App\Twig\Components;
338+
339+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
340+
341+
#[AsTwigComponent]
342+
final class Alert
343+
{
344+
public string $message;
345+
346+
public function formatMessage(): string
347+
{
348+
return strtoupper($this->message);
349+
}
350+
351+
private function helperMethod(): void
352+
{
353+
// ...
354+
}
355+
}
356+
```
357+
358+
:+1:
359+
360+
<br>
361+
303362
### PublicPropertiesShouldBeCamelCaseRule
304363

305364
Enforces that all public properties in Twig Components follow camelCase naming convention.

ecs.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
declare(strict_types=1);
44

5+
use PhpCsFixer\Fixer\ClassNotation\ProtectedToPrivateFixer;
56
use Symplify\EasyCodingStandard\Config\ECSConfig;
67

78
return ECSConfig::configure()
@@ -11,4 +12,9 @@
1112
])
1213
->withRootFiles()
1314
->withPreparedSets(psr12: true, common: true)
15+
->withSkip([
16+
ProtectedToPrivateFixer::class => [
17+
__DIR__ . '/tests/Rules/TwigComponent/MethodsShouldBePublicOrPrivateRule/Fixture/*',
18+
],
19+
])
1420
;

phpstan-baseline.neon

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,15 @@ parameters:
1111
identifier: property.unused
1212
count: 1
1313
path: tests/Rules/TwigComponent/PublicPropertiesShouldBeCamelCaseRule/Fixture/ComponentWithPrivateNonCamelCaseProperties.php
14+
15+
-
16+
message: '#^Method Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\MethodsShouldBePublicOrPrivateRule\\Fixture\\ComponentWithPublicAndPrivateMethods\:\:privateMethod\(\) is unused\.$#'
17+
identifier: method.unused
18+
count: 1
19+
path: tests/Rules/TwigComponent/MethodsShouldBePublicOrPrivateRule/Fixture/ComponentWithPublicAndPrivateMethods.php
20+
21+
-
22+
message: '#^Method Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\MethodsShouldBePublicOrPrivateRule\\Fixture\\LiveComponentWithPublicAndPrivateMethods\:\:privateMethod\(\) is unused\.$#'
23+
identifier: method.unused
24+
count: 1
25+
path: tests/Rules/TwigComponent/MethodsShouldBePublicOrPrivateRule/Fixture/LiveComponentWithPublicAndPrivateMethods.php
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Rules\TwigComponent;
6+
7+
use Kocal\PHPStanSymfonyUX\NodeAnalyzer\AttributeFinder;
8+
use PhpParser\Node;
9+
use PhpParser\Node\Stmt\Class_;
10+
use PHPStan\Analyser\Scope;
11+
use PHPStan\Rules\Rule;
12+
use PHPStan\Rules\RuleErrorBuilder;
13+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
14+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
15+
16+
/**
17+
* @implements Rule<Class_>
18+
*/
19+
final class MethodsShouldBePublicOrPrivateRule implements Rule
20+
{
21+
public function getNodeType(): string
22+
{
23+
return Class_::class;
24+
}
25+
26+
public function processNode(Node $node, Scope $scope): array
27+
{
28+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
29+
return [];
30+
}
31+
32+
$errors = [];
33+
34+
foreach ($node->getMethods() as $method) {
35+
if ($method->isProtected()) {
36+
$methodName = $method->name->toString();
37+
38+
$errors[] = RuleErrorBuilder::message(
39+
sprintf('Method "%s()" in a Twig component should not be protected.', $methodName)
40+
)
41+
->identifier('symfonyUX.twigComponent.methodsShouldBePublicOrPrivate')
42+
->line($method->getLine())
43+
->tip('Twig component methods should be either public or private, not protected.')
44+
->build();
45+
}
46+
}
47+
48+
return $errors;
49+
}
50+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ComponentWithProtectedMethod
11+
{
12+
public string $name = '';
13+
14+
protected function protectedMethod(): void
15+
{
16+
}
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ComponentWithPublicAndPrivateMethods
11+
{
12+
public string $name = '';
13+
14+
public function publicMethod(): void
15+
{
16+
}
17+
18+
private function privateMethod(): void
19+
{
20+
}
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule\Fixture;
6+
7+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
8+
9+
#[AsLiveComponent]
10+
final class LiveComponentWithProtectedMethod
11+
{
12+
public string $name = '';
13+
14+
protected function protectedMethod(): void
15+
{
16+
}
17+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule\Fixture;
6+
7+
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
8+
9+
#[AsLiveComponent]
10+
final class LiveComponentWithPublicAndPrivateMethods
11+
{
12+
public string $name = '';
13+
14+
public function publicMethod(): void
15+
{
16+
}
17+
18+
private function privateMethod(): void
19+
{
20+
}
21+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule\Fixture;
6+
7+
final class NotAComponent
8+
{
9+
public string $name = '';
10+
11+
protected function protectedMethod(): void
12+
{
13+
}
14+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule;
6+
7+
use Kocal\PHPStanSymfonyUX\Rules\TwigComponent\MethodsShouldBePublicOrPrivateRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* @extends RuleTestCase<MethodsShouldBePublicOrPrivateRule>
13+
*/
14+
final class MethodsShouldBePublicOrPrivateRuleTest extends RuleTestCase
15+
{
16+
public function testViolations(): void
17+
{
18+
$this->analyse(
19+
[__DIR__ . '/Fixture/ComponentWithProtectedMethod.php'],
20+
[
21+
[
22+
'Method "protectedMethod()" in a Twig component should not be protected.',
23+
14,
24+
'Twig component methods should be either public or private, not protected.',
25+
],
26+
]
27+
);
28+
29+
$this->analyse(
30+
[__DIR__ . '/Fixture/LiveComponentWithProtectedMethod.php'],
31+
[
32+
[
33+
'Method "protectedMethod()" in a Twig component should not be protected.',
34+
14,
35+
'Twig component methods should be either public or private, not protected.',
36+
],
37+
]
38+
);
39+
}
40+
41+
public function testNoViolations(): void
42+
{
43+
$this->analyse(
44+
[__DIR__ . '/Fixture/NotAComponent.php'],
45+
[]
46+
);
47+
48+
$this->analyse(
49+
[__DIR__ . '/Fixture/ComponentWithPublicAndPrivateMethods.php'],
50+
[]
51+
);
52+
53+
$this->analyse(
54+
[__DIR__ . '/Fixture/LiveComponentWithPublicAndPrivateMethods.php'],
55+
[]
56+
);
57+
}
58+
59+
public static function getAdditionalConfigFiles(): array
60+
{
61+
return [__DIR__ . '/config/configured_rule.neon'];
62+
}
63+
64+
protected function getRule(): Rule
65+
{
66+
return self::getContainer()->getByType(MethodsShouldBePublicOrPrivateRule::class);
67+
}
68+
}

0 commit comments

Comments
 (0)