Skip to content

Commit a72c1ae

Browse files
committed
Add PublicPropertiesShouldBeCamelCaseRule
1 parent 82ed246 commit a72c1ae

12 files changed

+300
-1
lines changed

.gitattributes

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
/phpunit.dist.xml export-ignore
33
/tests export-ignore
44
/.editorconfig export-ignore
5-
/phpunit.dist.xml
5+
/AGENTS.md export-ignore
6+
/ecs.php export-ignore
7+
/phpstan.dist.neon export-ignore
8+
/phpstan-baseline.neon export-ignore
9+
/phpunit.dist.xml export-ignore

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,3 +220,62 @@ final class Alert
220220
:+1:
221221

222222
<br>
223+
224+
### PublicPropertiesShouldBeCamelCaseRule
225+
226+
Enforces that all public properties in Twig Components follow camelCase naming convention.
227+
This ensures consistency and better integration with Twig templates where properties are passed and accessed using camelCase.
228+
229+
```yaml
230+
rules:
231+
- Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PublicPropertiesShouldBeCamelCaseRule
232+
```
233+
234+
```php
235+
// src/Twig/Components/Alert.php
236+
namespace App\Twig\Components;
237+
238+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
239+
240+
#[AsTwigComponent]
241+
final class Alert
242+
{
243+
public string $user_name;
244+
public bool $is_active;
245+
}
246+
```
247+
248+
```php
249+
// src/Twig/Components/Alert.php
250+
namespace App\Twig\Components;
251+
252+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
253+
254+
#[AsTwigComponent]
255+
final class Alert
256+
{
257+
public string $UserName;
258+
}
259+
```
260+
261+
:x:
262+
263+
<br>
264+
265+
```php
266+
// src/Twig/Components/Alert.php
267+
namespace App\Twig\Components;
268+
269+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
270+
271+
#[AsTwigComponent]
272+
final class Alert
273+
{
274+
public string $userName;
275+
public bool $isActive;
276+
}
277+
```
278+
279+
:+1:
280+
281+
<br>

phpstan-baseline.neon

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Property Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\PublicPropertiesShouldBeCamelCaseRule\\Fixture\\ComponentWithPrivateNonCamelCaseProperties\:\:\$is_active is unused\.$#'
5+
identifier: property.unused
6+
count: 1
7+
path: tests/Rules/TwigComponent/PublicPropertiesShouldBeCamelCaseRule/Fixture/ComponentWithPrivateNonCamelCaseProperties.php
8+
9+
-
10+
message: '#^Property Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\PublicPropertiesShouldBeCamelCaseRule\\Fixture\\ComponentWithPrivateNonCamelCaseProperties\:\:\$user_name is unused\.$#'
11+
identifier: property.unused
12+
count: 1
13+
path: tests/Rules/TwigComponent/PublicPropertiesShouldBeCamelCaseRule/Fixture/ComponentWithPrivateNonCamelCaseProperties.php

phpstan.dist.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
include:
2+
- ./phpstan-baseline.neon
3+
14
parameters:
25
level: max
36

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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 PublicPropertiesShouldBeCamelCaseRule 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+
$errors = [];
32+
33+
foreach ($node->getProperties() as $property) {
34+
if (! $property->isPublic()) {
35+
continue;
36+
}
37+
38+
foreach ($property->props as $prop) {
39+
$propertyName = $prop->name->toString();
40+
41+
if (! $this->isCamelCase($propertyName)) {
42+
$errors[] = RuleErrorBuilder::message(
43+
sprintf('Public property "%s" in a Twig component should be in camelCase.', $propertyName)
44+
)
45+
->identifier('symfonyUX.twigComponent.publicPropertiesShouldBeCamelCase')
46+
->line($property->getLine())
47+
->tip(sprintf('Consider renaming "%s" to "%s".', $propertyName, $this->toCamelCase($propertyName)))
48+
->build();
49+
}
50+
}
51+
}
52+
53+
return $errors;
54+
}
55+
56+
private function isCamelCase(string $name): bool
57+
{
58+
// Property should start with a lowercase letter and contain no underscores
59+
return preg_match('/^[a-z][a-zA-Z0-9]*$/', $name) === 1;
60+
}
61+
62+
private function toCamelCase(string $name): string
63+
{
64+
// Convert snake_case or PascalCase to camelCase
65+
$name = str_replace('_', ' ', $name);
66+
$name = ucwords($name);
67+
$name = str_replace(' ', '', $name);
68+
69+
return lcfirst($name);
70+
}
71+
}
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\PublicPropertiesShouldBeCamelCaseRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ComponentWithCamelCaseProperties
11+
{
12+
public string $userName;
13+
14+
public bool $isActive;
15+
16+
public int $count;
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PublicPropertiesShouldBeCamelCaseRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ComponentWithPascalCaseProperty
11+
{
12+
public string $UserName;
13+
}
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\PublicPropertiesShouldBeCamelCaseRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ComponentWithPrivateNonCamelCaseProperties
11+
{
12+
public string $validProperty;
13+
14+
private string $user_name;
15+
16+
private bool $is_active;
17+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Kocal\PHPStanSymfonyUX\Tests\Rules\TwigComponent\PublicPropertiesShouldBeCamelCaseRule\Fixture;
6+
7+
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
8+
9+
#[AsTwigComponent]
10+
final class ComponentWithSnakeCaseProperty
11+
{
12+
public string $user_name;
13+
14+
public bool $is_active;
15+
}
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\PublicPropertiesShouldBeCamelCaseRule\Fixture;
6+
7+
final class NotAComponent
8+
{
9+
public string $user_name;
10+
11+
public bool $is_active;
12+
}

0 commit comments

Comments
 (0)