diff --git a/README.md b/README.md index 5e8ffc3..c5e349f 100644 --- a/README.md +++ b/README.md @@ -404,3 +404,94 @@ final class Alert :+1:
+ +### PreMountMethodSignatureRule + +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`. +This ensures proper integration with the Symfony UX TwigComponent lifecycle hooks. + +```yaml +rules: + - Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PreMountMethodSignatureRule +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +use Symfony\UX\TwigComponent\Attribute\PreMount; + +#[AsTwigComponent] +final class Alert +{ + #[PreMount] + protected function preMount(array $data): array + { + return $data; + } +} +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +use Symfony\UX\TwigComponent\Attribute\PreMount; + +#[AsTwigComponent] +final class Alert +{ + #[PreMount] + public function preMount(array $data): void + { + } +} +``` + +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +use Symfony\UX\TwigComponent\Attribute\PreMount; + +#[AsTwigComponent] +final class Alert +{ + #[PreMount] + public function preMount(string $data): array + { + return []; + } +} +``` + +:x: + +
+ +```php +// src/Twig/Components/Alert.php +namespace App\Twig\Components; + +use Symfony\UX\TwigComponent\Attribute\AsTwigComponent; +use Symfony\UX\TwigComponent\Attribute\PreMount; + +#[AsTwigComponent] +final class Alert +{ + #[PreMount] + public function preMount(array $data): array + { + $data['timestamp'] = time(); + + return $data; + } +} +``` + +:+1: + +
diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 42b264f..0000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,25 +0,0 @@ -parameters: - ignoreErrors: - - - message: '#^Property Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\PublicPropertiesShouldBeCamelCaseRule\\Fixture\\ComponentWithPrivateNonCamelCaseProperties\:\:\$is_active is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Rules/TwigComponent/PublicPropertiesShouldBeCamelCaseRule/Fixture/ComponentWithPrivateNonCamelCaseProperties.php - - - - message: '#^Property Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\PublicPropertiesShouldBeCamelCaseRule\\Fixture\\ComponentWithPrivateNonCamelCaseProperties\:\:\$user_name is unused\.$#' - identifier: property.unused - count: 1 - path: tests/Rules/TwigComponent/PublicPropertiesShouldBeCamelCaseRule/Fixture/ComponentWithPrivateNonCamelCaseProperties.php - - - - message: '#^Method Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\MethodsShouldBePublicOrPrivateRule\\Fixture\\ComponentWithPublicAndPrivateMethods\:\:privateMethod\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Rules/TwigComponent/MethodsShouldBePublicOrPrivateRule/Fixture/ComponentWithPublicAndPrivateMethods.php - - - - message: '#^Method Kocal\\PHPStanSymfonyUX\\Tests\\Rules\\TwigComponent\\MethodsShouldBePublicOrPrivateRule\\Fixture\\LiveComponentWithPublicAndPrivateMethods\:\:privateMethod\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: tests/Rules/TwigComponent/MethodsShouldBePublicOrPrivateRule/Fixture/LiveComponentWithPublicAndPrivateMethods.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ea25a23..e786f4c 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,9 +1,17 @@ -includes: - - ./phpstan-baseline.neon - parameters: level: max paths: - src - tests + + ignoreErrors: + - + paths: + - tests/**/Fixture/* + identifiers: + - method.unused + - missingType.iterableValue + - missingType.return + - property.unused + - return.unusedType \ No newline at end of file diff --git a/src/Rules/TwigComponent/PreMountMethodSignatureRule.php b/src/Rules/TwigComponent/PreMountMethodSignatureRule.php new file mode 100644 index 0000000..4543c34 --- /dev/null +++ b/src/Rules/TwigComponent/PreMountMethodSignatureRule.php @@ -0,0 +1,115 @@ + + */ +final class PreMountMethodSignatureRule implements Rule +{ + public function getNodeType(): string + { + return Class_::class; + } + + public function processNode(Node $node, Scope $scope): array + { + if (! AttributeFinder::findAnyAttribute($node, [AsTwigComponent::class, AsLiveComponent::class])) { + return []; + } + + $errors = []; + + foreach ($node->getMethods() as $method) { + // Check if the method has the PreMount attribute + if (! AttributeFinder::findAttribute($method, PreMount::class)) { + continue; + } + + $errors = array_merge($errors, $this->validatePreMountMethod($method)); + } + + return $errors; + } + + /** + * @return list<\PHPStan\Rules\IdentifierRuleError> + */ + private function validatePreMountMethod(Node\Stmt\ClassMethod $node): array + { + $errors = []; + + // Check if the method is public + if (! $node->isPublic()) { + $errors[] = RuleErrorBuilder::message( + sprintf('Method "%s" with #[PreMount] attribute must be public.', $node->name->toString()) + ) + ->identifier('symfonyUX.twigComponent.preMountMethodMustBePublic') + ->line($node->getLine()) + ->tip('Change the method visibility to public.') + ->build(); + } + + // Check the return type + $returnType = $node->getReturnType(); + if ($returnType === null) { + $errors[] = RuleErrorBuilder::message( + sprintf('Method "%s" with #[PreMount] attribute must have a return type of "array".', $node->name->toString()) + ) + ->identifier('symfonyUX.twigComponent.preMountMethodMissingReturnType') + ->line($node->getLine()) + ->tip('Add ": array" return type to the method.') + ->build(); + } else { + $isValidReturnType = $returnType instanceof Node\Identifier && $returnType->toString() === 'array'; + + if (! $isValidReturnType) { + $errors[] = RuleErrorBuilder::message( + sprintf('Method "%s" with #[PreMount] attribute must have a return type of "array".', $node->name->toString()) + ) + ->identifier('symfonyUX.twigComponent.preMountMethodInvalidReturnType') + ->line($returnType->getLine()) + ->tip('Change the return type to ": array".') + ->build(); + } + } + + // Check that there is exactly one parameter of type array + if (count($node->params) !== 1) { + $errors[] = RuleErrorBuilder::message( + sprintf('Method "%s" with #[PreMount] attribute must have exactly one parameter of type "array".', $node->name->toString()) + ) + ->identifier('symfonyUX.twigComponent.preMountMethodInvalidParameterCount') + ->line($node->getLine()) + ->tip('The method should have exactly one parameter: "array $data".') + ->build(); + } else { + $param = $node->params[0]; + $paramType = $param->type; + + if (! $paramType instanceof Node\Identifier || $paramType->toString() !== 'array') { + $errors[] = RuleErrorBuilder::message( + sprintf('Method "%s" with #[PreMount] attribute must have a parameter of type "array".', $node->name->toString()) + ) + ->identifier('symfonyUX.twigComponent.preMountMethodInvalidParameterType') + ->line($param->getLine()) + ->tip('Change the parameter type to "array".') + ->build(); + } + } + + return $errors; + } +} diff --git a/tests/Rules/TwigComponent/PreMountMethodSignatureRule/Fixture/InvalidNoParameter.php b/tests/Rules/TwigComponent/PreMountMethodSignatureRule/Fixture/InvalidNoParameter.php new file mode 100644 index 0000000..301e101 --- /dev/null +++ b/tests/Rules/TwigComponent/PreMountMethodSignatureRule/Fixture/InvalidNoParameter.php @@ -0,0 +1,18 @@ + + */ +final class PreMountMethodSignatureRuleTest extends RuleTestCase +{ + public function testViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/InvalidNotPublic.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must be public.', + 13, + 'Change the method visibility to public.', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/InvalidNoReturnType.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must have a return type of "array".', + 13, + 'Add ": array" return type to the method.', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/InvalidWrongReturnType.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must have a return type of "array".', + 14, + 'Change the return type to ": array".', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/InvalidNoParameter.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must have exactly one parameter of type "array".', + 13, + 'The method should have exactly one parameter: "array $data".', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/InvalidTooManyParameters.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must have exactly one parameter of type "array".', + 13, + 'The method should have exactly one parameter: "array $data".', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/InvalidWithNullableReturnType.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must have a return type of "array".', + 14, + 'Change the return type to ": array".', + ], + ] + ); + + $this->analyse( + [__DIR__ . '/Fixture/InvalidWrongParameterType.php'], + [ + [ + 'Method "preMount" with #[PreMount] attribute must have a parameter of type "array".', + 14, + 'Change the parameter type to "array".', + ], + ] + ); + } + + public function testNoViolations(): void + { + $this->analyse( + [__DIR__ . '/Fixture/NotAComponent.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/ValidTwigComponent.php'], + [] + ); + + $this->analyse( + [__DIR__ . '/Fixture/ValidLiveComponent.php'], + [] + ); + } + + public static function getAdditionalConfigFiles(): array + { + return [__DIR__ . '/config/configured_rule.neon']; + } + + protected function getRule(): Rule + { + return self::getContainer()->getByType(PreMountMethodSignatureRule::class); + } +} diff --git a/tests/Rules/TwigComponent/PreMountMethodSignatureRule/config/configured_rule.neon b/tests/Rules/TwigComponent/PreMountMethodSignatureRule/config/configured_rule.neon new file mode 100644 index 0000000..defbfce --- /dev/null +++ b/tests/Rules/TwigComponent/PreMountMethodSignatureRule/config/configured_rule.neon @@ -0,0 +1,2 @@ +rules: + - Kocal\PHPStanSymfonyUX\Rules\TwigComponent\PreMountMethodSignatureRule