diff --git a/rules-tests/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector/Fixture/with_options_argument.php.inc b/rules-tests/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector/Fixture/with_options_argument.php.inc new file mode 100644 index 000000000..105871f8d --- /dev/null +++ b/rules-tests/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector/Fixture/with_options_argument.php.inc @@ -0,0 +1,139 @@ +withEnvironment(...), ['needs_environment' => true]), + new \Twig\TwigFilter('with_context', [$this, 'withContext'], ['needs_context' => true]), + new \Twig\TwigFilter('with_charset', [$this, 'withCharset'], ['needs_charset' => true]), + new \Twig\TwigFilter('with_pre_escape', [$this, 'withPreEscape'], ['pre_escape' => 'html']), + new \Twig\TwigFilter('with_preserves_safety', [$this, 'withPreservesSafety'], ['preserves_safety' => ['html']]), + new \Twig\TwigFilter('with_safe_callback', [$this, 'withSafeCallback'], ['is_safe_callback' => [self::class, 'checkSafeCallback']]), + new \Twig\TwigFilter('with_deprecation_info', [$this, 'withDeprecationInfo'], ['deprecation_info' => new DeprecatedCallableInfo('package', 'version')]), + new \Twig\TwigFilter('with_everything', [$this, 'withEverything'], ['is_safe' => ['html'], 'needs_context' => true, 'needs_charset' => true, 'needs_environment' => true, 'pre_escape' => 'html', 'preserves_safety' => ['html']],), + ]; + } + + public function withEnvironment(Environment $env, $value) + { + return $value; + } + + public function withContext(array $context, $value) + { + return $value; + } + + public function withCharset(string $charset, $value) + { + return $value; + } + + public function withPreEscape($value) + { + return $value; + } + + public function withPreservesSafety($value) + { + return $value; + } + + public function withSafeCallback($value) + { + return $value; + } + + public function withDeprecationInfo($value) + { + return $value; + } + + public function withEverything(string $charset, Environment $env, array $context, $value) + { + return $value; + } + + public function checkSafeCallback(Node $argsNode): array + { + return []; + } +} + +?> +----- + diff --git a/rules-tests/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector/Fixture/with_options_argument.php.inc b/rules-tests/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector/Fixture/with_options_argument.php.inc new file mode 100644 index 000000000..c82d90888 --- /dev/null +++ b/rules-tests/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector/Fixture/with_options_argument.php.inc @@ -0,0 +1,115 @@ +withEnvironment(...), ['needs_environment' => true]), + new \Twig\TwigFunction('with_context', [$this, 'withContext'], ['needs_context' => true]), + new \Twig\TwigFunction('with_charset', [$this, 'withCharset'], ['needs_charset' => true]), + new \Twig\TwigFunction('with_safe_callback', [$this, 'withSafeCallback'], ['is_safe_callback' => [self::class, 'checkSafeCallback']]), + new \Twig\TwigFunction('with_deprecation_info', [$this, 'withDeprecationInfo'], ['deprecation_info' => new DeprecatedCallableInfo('package', 'version')]), + new \Twig\TwigFunction('with_everything', [$this, 'withEverything'], ['is_safe' => ['html'], 'needs_context' => true, 'needs_charset' => true, 'needs_environment' => true]), + ]; + } + + public function withEnvironment(Environment $env, $value) + { + return $value; + } + + public function withContext(array $context, $value) + { + return $value; + } + + public function withCharset(string $charset, $value) + { + return $value; + } + + public function withSafeCallback($value) + { + return $value; + } + + public function withDeprecationInfo($value) + { + return $value; + } + + public function withEverything(string $charset, Environment $env, array $context, $value) + { + return $value; + } + + public function checkSafeCallback(Node $argsNode): array + { + return []; + } +} + +?> +----- + diff --git a/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php b/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php index 8c32d874d..5b606cb7d 100644 --- a/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php +++ b/rules/Symfony73/GetMethodToAsTwigAttributeTransformer.php @@ -11,6 +11,7 @@ use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Expr\New_; +use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Class_; @@ -28,6 +29,15 @@ */ final readonly class GetMethodToAsTwigAttributeTransformer { + private const OPTION_TO_NAMED_ARG = [ + 'is_safe' => 'isSafe', + 'needs_environment' => 'needsEnvironment', + 'needs_context' => 'needsContext', + 'needs_charset' => 'needsCharset', + 'is_safe_callback' => 'isSafeCallback', + 'deprecation_info' => 'deprecationInfo', + ]; + public function __construct( private LocalArrayMethodCallableMatcher $localArrayMethodCallableMatcher, private ReturnEmptyArrayMethodRemover $returnEmptyArrayMethodRemover, @@ -36,11 +46,15 @@ public function __construct( ) { } + /** + * @param array $additionalOptionMapping + */ public function transformClassGetMethodToAttributeMarker( Class_ $class, string $methodName, string $attributeClass, - ObjectType $objectType + ObjectType $objectType, + array $additionalOptionMapping = [] ): bool { // check if attribute even exists @@ -77,7 +91,8 @@ public function transformClassGetMethodToAttributeMarker( } $new = $arrayItem->value; - if (count($new->getArgs()) !== 2) { + $argCount = count($new->getArgs()); + if ($argCount > 3 || $argCount < 2) { continue; } @@ -87,6 +102,7 @@ public function transformClassGetMethodToAttributeMarker( } $secondArg = $new->getArgs()[1]; + $thirdArg = $new->getArgs()[2] ?? null; if ($this->isLocalCallable($secondArg->value)) { $localMethodName = $this->localArrayMethodCallableMatcher->match($secondArg->value, $objectType); @@ -100,7 +116,12 @@ public function transformClassGetMethodToAttributeMarker( continue; } - $this->decorateMethodWithAttribute($localMethod, $attributeClass, $nameArg); + $optionArguments = $this->getArgumentsFromOptionArray($thirdArg, $additionalOptionMapping); + if ($optionArguments === null) { + continue; + } + + $this->decorateMethodWithAttribute($localMethod, $attributeClass, [$nameArg, ...$optionArguments]); $this->visibilityManipulator->makePublic($localMethod); // remove old new function instance @@ -120,9 +141,12 @@ public function transformClassGetMethodToAttributeMarker( return $hasChanged; } - private function decorateMethodWithAttribute(ClassMethod $classMethod, string $attributeClass, Arg $arg): void + /** + * @param Arg[] $args + */ + private function decorateMethodWithAttribute(ClassMethod $classMethod, string $attributeClass, array $args): void { - $classMethod->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified($attributeClass), [$arg])]); + $classMethod->attrGroups[] = new AttributeGroup([new Attribute(new FullyQualified($attributeClass), $args)]); } private function isLocalCallable(Expr $expr): bool @@ -133,4 +157,44 @@ private function isLocalCallable(Expr $expr): bool return $expr instanceof Array_ && count($expr->items) === 2; } + + /** + * @param array $additionalOptionMapping + * + * @return Arg[]|null + */ + private function getArgumentsFromOptionArray(?Arg $optionArgument, array $additionalOptionMapping): ?array + { + if (! $optionArgument?->value instanceof Array_) { + return []; + } + + $allOptionMappings = [...self::OPTION_TO_NAMED_ARG, ...$additionalOptionMapping]; + + $args = []; + foreach ($optionArgument->value->items as $item) { + if (! $item->key instanceof String_) { + continue; + } + + $mappedName = $allOptionMappings[$item->key->value] ?? null; + if ($mappedName === null) { + continue; + } + + if ($mappedName === 'isSafeCallback') { + if ($item->value instanceof MethodCall && $item->value->isFirstClassCallable()) { + continue; + } + } + + $arg = new Arg($item->value); + $arg->name = new Identifier($mappedName); + $args[] = $arg; + } + + $totalItems = count($optionArgument->value->items); + + return count($args) === $totalItems ? $args : null; + } } diff --git a/rules/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector.php b/rules/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector.php index 612dafba2..4e671f929 100644 --- a/rules/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector.php +++ b/rules/Symfony73/Rector/Class_/GetFiltersToAsTwigFilterAttributeRector.php @@ -33,17 +33,18 @@ public function getRuleDefinition(): RuleDefinition new CodeSample( <<<'CODE_SAMPLE' use Twig\Extension\AbstractExtension; +use Twig\Environment; class SomeClass extends AbstractExtension { public function getFilters() { return [ - new \Twig\TwigFilter('filter_name', [$this, 'localMethod']), + new \Twig\TwigFilter('filter_name', [$this, 'localMethod', 'needs_environment' => true]), ]; } - public function localMethod($value) + public function localMethod(Environment $env, $value) { return $value; } @@ -53,11 +54,12 @@ public function localMethod($value) <<<'CODE_SAMPLE' use Twig\Extension\AbstractExtension; use Twig\Attribute\AsTwigFilter; +use Twig\Environment; class SomeClass extends AbstractExtension { - #[TwigFilter('filter_name')] - public function localMethod($value) + #[TwigFilter('filter_name', needsEnvironment: true)] + public function localMethod(Environment $env, $value) { return $value; } @@ -90,7 +92,11 @@ public function refactor(Node $node): ?Class_ $node, 'getFilters', TwigClass::AS_TWIG_FILTER_ATTRIBUTE, - $twigExtensionObjectType + $twigExtensionObjectType, + [ + 'preserves_safety' => 'preservesSafety', + 'pre_escape' => 'preEscape', + ] ); if ($hasChanged) { diff --git a/rules/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector.php b/rules/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector.php index 0ef572f24..12fe5f54e 100644 --- a/rules/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector.php +++ b/rules/Symfony73/Rector/Class_/GetFunctionsToAsTwigFunctionAttributeRector.php @@ -33,17 +33,18 @@ public function getRuleDefinition(): RuleDefinition new CodeSample( <<<'CODE_SAMPLE' use Twig\Extension\AbstractExtension; +use Twig\Environment; class SomeClass extends AbstractExtension { public function getFunctions() { return [ - new \Twig\TwigFunction('function_name', [$this, 'localMethod']), + new \Twig\TwigFunction('function_name', [$this, 'localMethod', 'needs_environment' => true]), ]; } - public function localMethod($value) + public function localMethod(Environment $env, $value) { return $value; } @@ -53,11 +54,12 @@ public function localMethod($value) <<<'CODE_SAMPLE' use Twig\Extension\AbstractExtension; use Twig\Attribute\AsTwigFunction; +use Twig\Environment; class SomeClass extends AbstractExtension { - #[AsTwigFunction('function_name')] - public function localMethod($value) + #[AsTwigFunction('function_name', needsEnvironment: true)] + public function localMethod(Environment $env, $value) { return $value; }