From 67dc1b265f0e4fb71ab79b7e72d4c26d2fea4d30 Mon Sep 17 00:00:00 2001 From: Fabien Potencier Date: Sat, 14 Sep 2024 23:01:08 +0200 Subject: [PATCH] Implement as simpler and more robust algorithm --- src/Util/CallableArgumentsExtractor.php | 42 +++++++------------ tests/Util/CallableArgumentsExtractorTest.php | 23 ++++++---- 2 files changed, 31 insertions(+), 34 deletions(-) diff --git a/src/Util/CallableArgumentsExtractor.php b/src/Util/CallableArgumentsExtractor.php index d3b54ee406..4e6867e0e6 100644 --- a/src/Util/CallableArgumentsExtractor.php +++ b/src/Util/CallableArgumentsExtractor.php @@ -44,6 +44,7 @@ public function extractArguments(Node $arguments): array foreach ($arguments as $name => $node) { if (!\is_int($name)) { $named = true; + $name = $this->normalizeName($name); } elseif ($named) { throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -54,7 +55,7 @@ public function extractArguments(Node $arguments): array if (!$named && !$this->twigCallable->isVariadic()) { $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { - $argName = $this->toSnakeCase($this->toCamelCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName())); + $argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName()); throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -87,13 +88,9 @@ public function extractArguments(Node $arguments): array } $callableParameterNames[] = $callableParameterName; + $normalizedCallableParameterName = $this->normalizeName($callableParameterName); - if ( - \array_key_exists($resolvedParameterName = $callableParameterName, $extractedArguments) - || \array_key_exists($resolvedParameterName = $this->toCamelCase($callableParameterName), $extractedArguments) - // toSnakeCase() needs the camelCase string (as per Symfony implementation) - || \array_key_exists($resolvedParameterName = $this->toSnakeCase($resolvedParameterName), $extractedArguments) - ) { + if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) { if (\array_key_exists($pos, $extractedArguments)) { throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } @@ -101,13 +98,13 @@ public function extractArguments(Node $arguments): array if (\count($missingArguments)) { throw new SyntaxError(\sprintf( 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', - $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $this->toSnakeCases($callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) ), $this->node->getTemplateLine(), $this->node->getSourceContext()); } $arguments = array_merge($arguments, $optionalArguments); - $arguments[] = $extractedArguments[$resolvedParameterName]; - unset($extractedArguments[$resolvedParameterName]); + $arguments[] = $extractedArguments[$normalizedCallableParameterName]; + unset($extractedArguments[$normalizedCallableParameterName]); $optionalArguments = []; } elseif (\array_key_exists($pos, $extractedArguments)) { $arguments = array_merge($arguments, $optionalArguments); @@ -124,7 +121,7 @@ public function extractArguments(Node $arguments): array $missingArguments[] = $callableParameterName; } else { - throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($this->toCamelCase($callableParameterName)), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); } } @@ -157,7 +154,7 @@ public function extractArguments(Node $arguments): array throw new SyntaxError( \sprintf( 'Unknown argument%s "%s" for %s "%s(%s)".', - \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', $this->toSnakeCases($callableParameterNames)) + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)) ), $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() @@ -167,28 +164,19 @@ public function extractArguments(Node $arguments): array return $arguments; } - // logic from Symfony\Component\String\AbstractUnicodeString::camel() - private function toCamelCase(string $str): string + private function normalizeName(string $name): string { - return str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) use (&$i) { - return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); - }, preg_replace('/[^\pL0-9]++/u', ' ', $str))); + return strtolower(str_replace('_', '', $name)); } // logic from Symfony\Component\String\AbstractUnicodeString::snake() private function toSnakeCase(string $str): string { - return mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $str), 'UTF-8'); - } - - private function toSnakeCases(array $strs): array - { - $snakes = []; - foreach ($strs as $str) { - $snakes[] = $this->toSnakeCase($str); - } + $camel = str_replace(' ', '', preg_replace_callback('/\b.(?!\p{Lu})/u', static function ($m) use (&$i) { + return 1 === ++$i ? ('İ' === $m[0] ? 'i̇' : mb_strtolower($m[0], 'UTF-8')) : mb_convert_case($m[0], \MB_CASE_TITLE, 'UTF-8'); + }, preg_replace('/[^\pL0-9]++/u', ' ', $str))); - return $snakes; + return mb_strtolower(preg_replace(['/(\p{Lu}+)(\p{Lu}\p{Ll})/u', '/([\p{Ll}0-9])(\p{Lu})/u'], '\1_\2', $camel), 'UTF-8'); } private function getCallableParameters(): array diff --git a/tests/Util/CallableArgumentsExtractorTest.php b/tests/Util/CallableArgumentsExtractorTest.php index 8776901814..6cb6641ced 100644 --- a/tests/Util/CallableArgumentsExtractorTest.php +++ b/tests/Util/CallableArgumentsExtractorTest.php @@ -81,20 +81,29 @@ public function testGetArgumentsForStaticMethod() } /** - * @dataProvider getGetArgumentsForCamelSnakeCases + * @dataProvider getGetArgumentsConversionData */ - public function testGetArgumentsForCamelSnakeCases(array $args) + public function testGetArgumentsConversion($arg1, $arg2) { - $this->assertEquals(['Fabien', 'Paris'], $this->getArguments('custom_static_function', [$this, 'customFunctionSnakeCamel'], $args)); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg1 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg2) => '';"), [$arg2 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg1) => '';"), [$arg2 => null])); + $this->assertEquals([null], $this->getArguments('custom', eval("return fn (\$$arg2) => '';"), [$arg1 => null])); } - public static function getGetArgumentsForCamelSnakeCases() + public static function getGetArgumentsConversionData() { - yield [['someName' => 'Fabien', 'someCity' => 'Paris']]; - yield [['some_name' => 'Fabien', 'some_city' => 'Paris']]; + yield ['some_name', 'some_name']; + yield ['someName', 'some_name']; + yield ['no_svg', 'noSVG']; + yield ['error_404', 'error404']; + yield ['errCode_404', 'err_code_404']; + yield ['errCode404', 'err_code_404']; + yield ['aBc', 'a_b_c']; + yield ['aBC', 'a_b_c']; } - public function testGetArgumentsForCamelSnakeCasesError() + public function testGetArgumentsError() { $this->expectException(SyntaxError::class); $this->expectExceptionMessage('Value for argument "some_name" is required for function "custom_static_function".');