Skip to content

Commit 03175df

Browse files
committed
Add PreMountMethodSignatureRule
1 parent b1759ab commit 03175df

16 files changed

+524
-28
lines changed

README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,3 +404,94 @@ final class Alert
404404
:+1:
405405

406406
<br>
407+
408+
### PreMountMethodSignatureRule
409+
410+
Enforces that methods with the `#[PreMount]` attribute have the correct signature: they must be public and have exactly one parameter of type `array`, with a return type of `array`.
411+
This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks.
412+
413+
```yaml
414+
rules:
415+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PreMountMethodSignatureRule
416+
```
417+
418+
```php
419+
// src/Twig/Components/Alert.php
420+
namespace App\Twig\Components;
421+
422+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
423+
use Symfony\UX\TwigComponent\Attribute\PreMount;
424+
425+
#[AsTwigComponent]
426+
final class Alert
427+
{
428+
#[PreMount]
429+
protected function preMount(array $data): array
430+
{
431+
return $data;
432+
}
433+
}
434+
```
435+
436+
```php
437+
// src/Twig/Components/Alert.php
438+
namespace App\Twig\Components;
439+
440+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
441+
use Symfony\UX\TwigComponent\Attribute\PreMount;
442+
443+
#[AsTwigComponent]
444+
final class Alert
445+
{
446+
#[PreMount]
447+
public function preMount(array $data): void
448+
{
449+
}
450+
}
451+
```
452+
453+
```php
454+
// src/Twig/Components/Alert.php
455+
namespace App\Twig\Components;
456+
457+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
458+
use Symfony\UX\TwigComponent\Attribute\PreMount;
459+
460+
#[AsTwigComponent]
461+
final class Alert
462+
{
463+
#[PreMount]
464+
public function preMount(string $data): array
465+
{
466+
return [];
467+
}
468+
}
469+
```
470+
471+
:x:
472+
473+
<br>
474+
475+
```php
476+
// src/Twig/Components/Alert.php
477+
namespace App\Twig\Components;
478+
479+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
480+
use Symfony\UX\TwigComponent\Attribute\PreMount;
481+
482+
#[AsTwigComponent]
483+
final class Alert
484+
{
485+
#[PreMount]
486+
public function preMount(array $data): array
487+
{
488+
$data['timestamp'] = time();
489+
490+
return $data;
491+
}
492+
}
493+
```
494+
495+
:+1:
496+
497+
<br>

phpstan-baseline.neon

Lines changed: 0 additions & 25 deletions
This file was deleted.

phpstan.dist.neon

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
1-
includes:
2-
- ./phpstan-baseline.neon
3-
41
parameters:
52
level: max
63

74
paths:
85
- src
96
- tests
7+
8+
ignoreErrors:
9+
-
10+
paths:
11+
- tests/**/Fixture/*
12+
identifiers:
13+
- method.unused
14+
- missingType.iterableValue
15+
- missingType.return
16+
- property.unused
17+
- return.unusedType
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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+
use Symfony\UX\TwigComponent\Attribute\PreMount;
16+
17+
/**
18+
* @implements Rule<Class_>
19+
*/
20+
final class PreMountMethodSignatureRule implements Rule
21+
{
22+
public function getNodeType(): string
23+
{
24+
return Class_::class;
25+
}
26+
27+
public function processNode(Node $node, Scope $scope): array
28+
{
29+
if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) {
30+
return [];
31+
}
32+
33+
$errors = [];
34+
35+
// Check all methods in the class
36+
foreach ($node->stmts as $stmt) {
37+
if (! $stmt instanceof Node\Stmt\ClassMethod) {
38+
continue;
39+
}
40+
41+
// Check if the method has the PreMount attribute
42+
if (! AttributeFinder::findAttribute($stmt, PreMount::class)) {
43+
continue;
44+
}
45+
46+
$errors = array_merge($errors, $this->validatePreMountMethod($stmt));
47+
}
48+
49+
return $errors;
50+
}
51+
52+
/**
53+
* @return list<\PHPStan\Rules\IdentifierRuleError>
54+
*/
55+
private function validatePreMountMethod(Node\Stmt\ClassMethod $node): array
56+
{
57+
$errors = [];
58+
59+
// Check if the method is public
60+
if (! $node->isPublic()) {
61+
$errors[] = RuleErrorBuilder::message(
62+
sprintf('Method "%s" with #[PreMount] attribute must be public.', $node->name->toString())
63+
)
64+
->identifier('symfonyUX.twigComponent.preMountMethodMustBePublic')
65+
->line($node->getLine())
66+
->tip('Change the method visibility to public.')
67+
->build();
68+
}
69+
70+
// Check the return type
71+
$returnType = $node->getReturnType();
72+
if ($returnType === null) {
73+
$errors[] = RuleErrorBuilder::message(
74+
sprintf('Method "%s" with #[PreMount] attribute must have a return type of "array".', $node->name->toString())
75+
)
76+
->identifier('symfonyUX.twigComponent.preMountMethodMissingReturnType')
77+
->line($node->getLine())
78+
->tip('Add ": array" return type to the method.')
79+
->build();
80+
} else {
81+
$isValidReturnType = $returnType instanceof Node\Identifier && $returnType->toString() === 'array';
82+
83+
if (! $isValidReturnType) {
84+
$errors[] = RuleErrorBuilder::message(
85+
sprintf('Method "%s" with #[PreMount] attribute must have a return type of "array".', $node->name->toString())
86+
)
87+
->identifier('symfonyUX.twigComponent.preMountMethodInvalidReturnType')
88+
->line($returnType->getLine())
89+
->tip('Change the return type to ": array".')
90+
->build();
91+
}
92+
}
93+
94+
// Check that there is exactly one parameter of type array
95+
if (count($node->params) !== 1) {
96+
$errors[] = RuleErrorBuilder::message(
97+
sprintf('Method "%s" with #[PreMount] attribute must have exactly one parameter of type "array".', $node->name->toString())
98+
)
99+
->identifier('symfonyUX.twigComponent.preMountMethodInvalidParameterCount')
100+
->line($node->getLine())
101+
->tip('The method should have exactly one parameter: "array $data".')
102+
->build();
103+
} else {
104+
$param = $node->params[0];
105+
$paramType = $param->type;
106+
107+
if (! $paramType instanceof Node\Identifier || $paramType->toString() !== 'array') {
108+
$errors[] = RuleErrorBuilder::message(
109+
sprintf('Method "%s" with #[PreMount] attribute must have a parameter of type "array".', $node->name->toString())
110+
)
111+
->identifier('symfonyUX.twigComponent.preMountMethodInvalidParameterType')
112+
->line($param->getLine())
113+
->tip('Change the parameter type to "array".')
114+
->build();
115+
}
116+
}
117+
118+
return $errors;
119+
}
120+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PreMountMethodSignatureRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
use Symfony\UX\TwigComponent\Attribute\PreMount;
9+
10+
#[AsTwigComponent]
11+
final class InvalidNoParameter
12+
{
13+
#[PreMount]
14+
public function preMount(): array
15+
{
16+
return [];
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PreMountMethodSignatureRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
use Symfony\UX\TwigComponent\Attribute\PreMount;
9+
10+
#[AsTwigComponent]
11+
final class InvalidNoReturnType
12+
{
13+
#[PreMount]
14+
public function preMount(array $data)
15+
{
16+
return $data;
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PreMountMethodSignatureRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
use Symfony\UX\TwigComponent\Attribute\PreMount;
9+
10+
#[AsTwigComponent]
11+
final class InvalidNotPublic
12+
{
13+
#[PreMount]
14+
private function preMount(array $data): array
15+
{
16+
return $data;
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PreMountMethodSignatureRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
use Symfony\UX\TwigComponent\Attribute\PreMount;
9+
10+
#[AsTwigComponent]
11+
final class InvalidTooManyParameters
12+
{
13+
#[PreMount]
14+
public function preMount(array $data, string $extra): array
15+
{
16+
return $data;
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PreMountMethodSignatureRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
use Symfony\UX\TwigComponent\Attribute\PreMount;
9+
10+
#[AsTwigComponent]
11+
final class InvalidWithNullableReturnType
12+
{
13+
#[PreMount]
14+
public function preMount(array $data): ?array
15+
{
16+
return $data;
17+
}
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PreMountMethodSignatureRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
use Symfony\UX\TwigComponent\Attribute\PreMount;
9+
10+
#[AsTwigComponent]
11+
final class InvalidWrongParameterType
12+
{
13+
#[PreMount]
14+
public function preMount(string $data): array
15+
{
16+
return [];
17+
}
18+
}

0 commit comments

Comments
 (0)