Skip to content

Commit 98c95e9

Browse files
committed
Add ComponentClassNameShouldNotEndWithComponentRule
1 parent 95698cb commit 98c95e9

File tree

8 files changed

+401
-0
lines changed

8 files changed

+401
-0
lines changed

AGENTS.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
# AI Agent Instructions - PHPStan Symfony UX Project
2+
3+
## Project Overview
4+
5+
This project contains custom PHPStan rules to improve static analysis of Symfony UX applications, particularly for Twig components.
6+
7+
## Project Structure
8+
9+
- `src/Rules/<UX Package>/` : Contains PHPStan rules for a given UX package (e.g.: `src/Rules/TwigComponent/`)
10+
- `src/NodeAnalyzer/` : Contains reusable analyzers (e.g., `AttributeFinder`)
11+
- `tests/Rules/<UX Package>/` : Contains tests for each rule for a given UX package (e.g.: `tests/Rules/TwigComponent/`)
12+
- `README.md` : Documentation of available rules
13+
14+
## How to Create a New PHPStan Rule
15+
16+
The code examples below are mainly written for TwigComponent, but it must be adapted:
17+
- the rules are organized by UX Packages
18+
- some code or files are maybe not necessary for other UX Packages
19+
20+
### 1. Create the rule class in `src/Rules/<UX Package>/`
21+
22+
Each rule must:
23+
- Implement PHPStan's `Rule` interface
24+
- Return an array of errors via `RuleErrorBuilder`
25+
26+
Typical structure:
27+
```php
28+
<?php
29+
30+
declare(strict_types=1);
31+
32+
namespace Kocal\PHPStanSymfonyUX\Rules\TwigComponent;
33+
34+
use Kocal\PHPStanSymfonyUX\NodeAnalyzer\AttributeFinder;
35+
use PhpParser\Node;
36+
use PhpParser\Node\Stmt\Class_;
37+
use PHPStan\Analyser\Scope;
38+
use PHPStan\Rules\Rule;
39+
use PHPStan\Rules\RuleErrorBuilder;
40+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
41+
42+
/**
43+
* @implements Rule<Class_>
44+
*/
45+
final class MyRuleRule implements Rule
46+
{
47+
public function getNodeType(): string
48+
{
49+
return Class_::class;
50+
}
51+
52+
public function processNode(Node $node, Scope $scope): array
53+
{
54+
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
55+
return [];
56+
}
57+
58+
// Validation logic here
59+
60+
if ($errorCondition) {
61+
return [
62+
RuleErrorBuilder::message('Clear and descriptive error message.')
63+
->identifier('symfonyUX.twigComponent.uniqueIdentifier')
64+
->line($node->getLine())
65+
->tip('Suggestion to fix the issue.')
66+
->build(),
67+
];
68+
}
69+
70+
return [];
71+
}
72+
}
73+
```
74+
75+
### 2. Create tests in `tests/Rules/<UX Package>/`
76+
77+
Required structure:
78+
```
79+
tests/Rules/TwigComponent/MyRuleRule/
80+
├── MyRuleRuleTest.php
81+
├── Fixture/
82+
│ ├── InvalidCase.php (case that should fail)
83+
│ ├── ValidCase.php (case that should pass)
84+
│ └── NotAComponent.php (class without AsTwigComponent attribute)
85+
└── config/
86+
└── configured_rule.neon
87+
```
88+
89+
#### Main test file (`MyRuleRuleTest.php`):
90+
```php
91+
<?php
92+
93+
declare(strict_types=1);
94+
95+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\MyRuleRule;
96+
97+
use Kocal\PHPStanSymfonyUX\Rules\TwigComponent\MyRuleRule;
98+
use PHPStan\Rules\Rule;
99+
use PHPStan\Testing\RuleTestCase;
100+
101+
/**
102+
* @extends RuleTestCase<MyRuleRule>
103+
*/
104+
final class MyRuleRuleTest extends RuleTestCase
105+
{
106+
public function testViolations(): void
107+
{
108+
$this->analyse(
109+
[__DIR__ . '/Fixture/InvalidCase.php'],
110+
[
111+
[
112+
'Expected error message.',
113+
10, // Line number
114+
'Expected suggestion.',
115+
],
116+
]
117+
);
118+
}
119+
120+
public function testNoViolations(): void
121+
{
122+
$this->analyse(
123+
[__DIR__ . '/Fixture/NotAComponent.php'],
124+
[]
125+
);
126+
127+
$this->analyse(
128+
[__DIR__ . '/Fixture/ValidCase.php'],
129+
[]
130+
);
131+
}
132+
133+
public static function getAdditionalConfigFiles(): array
134+
{
135+
return [__DIR__ . '/config/configured_rule.neon'];
136+
}
137+
138+
protected function getRule(): Rule
139+
{
140+
return self::getContainer()->getByType(MyRuleRule::class);
141+
}
142+
}
143+
```
144+
145+
#### Configuration (`config/configured_rule.neon`):
146+
```yaml
147+
rules:
148+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\MyRuleRule
149+
```
150+
151+
#### Fixtures:
152+
- **InvalidCase.php**: Example that violates the rule
153+
- **ValidCase.php**: Example that complies with the rule
154+
- **NotAComponent.php**: Class without `#[AsTwigComponent]` (should not trigger an error)
155+
156+
### 3. Document the rule in `README.md`
157+
158+
Add a new section under `## TwigComponent Rules`:
159+
```markdown
160+
### MyRuleRule
161+
162+
Clear description of what the rule checks and why.
163+
164+
\`\`\`yaml
165+
rules:
166+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\MyRuleRule
167+
\`\`\`
168+
169+
\`\`\`php
170+
// Invalid code example
171+
#[AsTwigComponent]
172+
final class BadExample
173+
{
174+
}
175+
\`\`\`
176+
177+
:x:
178+
179+
<br>
180+
181+
\`\`\`php
182+
// Valid code example
183+
#[AsTwigComponent]
184+
final class GoodExample
185+
{
186+
}
187+
\`\`\`
188+
189+
:+1:
190+
191+
<br>
192+
```
193+
194+
## Useful Commands
195+
196+
### Check and fix syntax + run tests
197+
```bash
198+
symfony composer qa-fix
199+
```
200+
201+
This command will:
202+
- Verify that changes are syntactically valid
203+
- Automatically fix code style issues
204+
- Run all tests to ensure they pass
205+
206+
### Other available commands
207+
Check the `composer.json` file to see all available commands.
208+
209+
## Best Practices
210+
211+
1. **Naming**: Rules should have a descriptive name and end with `Rule`
212+
2. **Identifiers**: Use the format `symfonyUX.twigComponent.descriptiveName` for error identifiers
213+
3. **Clear messages**: Error messages should be explicit and include a `tip()` with a suggestion
214+
4. **Complete tests**: Always test valid cases, invalid cases, and non-components
215+
5. **Documentation**: Document each rule in the README with concrete examples
216+
6. **Validation**: Always run `symfony composer qa-fix` before committing
217+
218+
## Examples of Existing Rules
219+
220+
- `ForbiddenAttributesPropertyRule`: Forbids the `$attributes` property
221+
- `ForbiddenClassPropertyRule`: Forbids the `$class` property
222+
- `ComponentClassNameShouldNotEndWithComponentRule`: Class names should not end with "Component"
223+
224+
These rules can serve as references for implementing new rules.

README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,47 @@ composer require --dev kocal/phpstan-symfony-ux
1212

1313
## TwigComponent Rules
1414

15+
### ComponentClassNameShouldNotEndWithComponentRule
16+
17+
Forbid Twig Component class names from ending with "Component" suffix, as it creates redundancy since the class is already identified as a component through the `#[AsTwigComponent]` attribute.
18+
19+
```yaml
20+
rules:
21+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ComponentClassNameShouldNotEndWithComponentRule
22+
```
23+
24+
```php
25+
// src/Twig/Components/AlertComponent.php
26+
namespace App\Twig\Components;
27+
28+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
29+
30+
#[AsTwigComponent]
31+
final class AlertComponent
32+
{
33+
}
34+
```
35+
36+
:x:
37+
38+
<br>
39+
40+
```php
41+
// src/Twig/Components/Alert.php
42+
namespace App\Twig\Components;
43+
44+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
45+
46+
#[AsTwigComponent]
47+
final class Alert
48+
{
49+
}
50+
```
51+
52+
:+1:
53+
54+
<br>
55+
1556
### ForbiddenAttributesPropertyRule
1657

1758
Forbid the use of the `$attributes` property in Twig Components, which can lead to confusion when using `{{ attributes }}` (an instance of `ComponentAttributes` that is automatically injected) in Twig templates.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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\TwigComponent\Attribute\AsTwigComponent;
14+
15+
/**
16+
* @implements Rule<Class_>
17+
*/
18+
final class ComponentClassNameShouldNotEndWithComponentRule implements Rule
19+
{
20+
public function getNodeType(): string
21+
{
22+
return Class_::class;
23+
}
24+
25+
public function processNode(Node $node, Scope $scope): array
26+
{
27+
if (! AttributeFinder::findAttribute($node, AsTwigComponent::class)) {
28+
return [];
29+
}
30+
31+
$className = $node->name;
32+
if ($className === null) {
33+
return [];
34+
}
35+
36+
$classNameString = $className->toString();
37+
if (str_ends_with($classNameString, 'Component')) {
38+
return [
39+
RuleErrorBuilder::message(sprintf('Twig component class "%s" should not end with "Component".', $classNameString))
40+
->identifier('symfonyUX.twigComponent.classNameShouldNotEndWithComponent')
41+
->line($className->getLine())
42+
->tip('Remove the "Component" suffix from the class name.')
43+
->build(),
44+
];
45+
}
46+
47+
return [];
48+
}
49+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ComponentClassNameShouldNotEndWithComponentRule;
6+
7+
use Kocal\PHPStanSymfonyUX\Rules\TwigComponent\ComponentClassNameShouldNotEndWithComponentRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* @extends RuleTestCase<ComponentClassNameShouldNotEndWithComponentRule>
13+
*/
14+
final class ComponentClassNameShouldNotEndWithComponentRuleTest extends RuleTestCase
15+
{
16+
public function testViolations(): void
17+
{
18+
$this->analyse(
19+
[__DIR__ . '/Fixture/InvalidComponentName.php'],
20+
[
21+
[
22+
'Twig component class "AlertComponent" should not end with "Component".',
23+
10,
24+
'Remove the "Component" suffix from the class name.',
25+
],
26+
]
27+
);
28+
}
29+
30+
public function testNoViolations(): void
31+
{
32+
$this->analyse(
33+
[__DIR__ . '/Fixture/NotAComponent.php'],
34+
[]
35+
);
36+
37+
$this->analyse(
38+
[__DIR__ . '/Fixture/ValidComponentName.php'],
39+
[]
40+
);
41+
}
42+
43+
public static function getAdditionalConfigFiles(): array
44+
{
45+
return [__DIR__ . '/config/configured_rule.neon'];
46+
}
47+
48+
protected function getRule(): Rule
49+
{
50+
return self::getContainer()->getByType(ComponentClassNameShouldNotEndWithComponentRule::class);
51+
}
52+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ComponentClassNameShouldNotEndWithComponentRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class AlertComponent
11+
{
12+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\ComponentClassNameShouldNotEndWithComponentRule\Fixture;
6+
7+
final class NotAComponent
8+
{
9+
}

0 commit comments

Comments
 (0)