Skip to content

Commit

Permalink
Implement as simpler and more robust algorithm
Browse files Browse the repository at this point in the history
  • Loading branch information
fabpot committed Sep 14, 2024
1 parent 3149ab3 commit 7ab7d46
Show file tree
Hide file tree
Showing 2 changed files with 33 additions and 34 deletions.
44 changes: 17 additions & 27 deletions src/Util/CallableArgumentsExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public function __construct(
public function extractArguments(Node $arguments): array
{
$extractedArguments = [];
$normalizedExtractedArguments = [];
$named = false;
foreach ($arguments as $name => $node) {
if (!\is_int($name)) {
Expand All @@ -49,12 +50,13 @@ public function extractArguments(Node $arguments): array
}

$extractedArguments[$name] = $node;
$normalizedExtractedArguments[$this->normalizeName($name)] = $name;
}

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());
}
Expand Down Expand Up @@ -87,27 +89,24 @@ 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, $normalizedExtractedArguments)) {
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());
}

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]);
$realName = $normalizedExtractedArguments[$normalizedCallableParameterName];
$arguments[] = $extractedArguments[$realName];
unset($extractedArguments[$realName]);
$optionalArguments = [];
} elseif (\array_key_exists($pos, $extractedArguments)) {
$arguments = array_merge($arguments, $optionalArguments);
Expand All @@ -124,7 +123,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());
}
}

Expand Down Expand Up @@ -157,7 +156,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()
Expand All @@ -167,28 +166,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] ? '' : 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] ? '' : 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
Expand Down
23 changes: 16 additions & 7 deletions tests/Util/CallableArgumentsExtractorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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".');
Expand Down

0 comments on commit 7ab7d46

Please sign in to comment.