in src/Migrations/HSLMigration.hack [314:461]
protected async function maybeMutateArgumentsAsync(
Node $root,
FunctionCallExpression $node,
?vec<int> $argument_order,
string $path,
keyset<HslNamespace> $found_namespaces,
): Awaitable<(?FunctionCallExpression, keyset<HslNamespace>)> {
$argument_list = $node->getArgumentList();
invariant($argument_list !== null, 'Function must have arguments');
$arguments = $argument_list->getChildren();
$new_argument_list = $argument_list;
$items = Vec\map($arguments, $argument ==> $argument->getItemx());
// can't handle these ones with wrong number of args yet
// return null, signaling to caller to skip rewriting this invocation
if (
$argument_order !== null && C\count($items) !== C\count($argument_order)
) {
return tuple(null, $found_namespaces);
}
// implode argument order is ambiguous
// when converting to join, check to make sure the second element is a string
// if the arguments are in the wrong order, reverse them
// if neither arg is a string, hh_client should complain so we just leave it as is
$fn_name = $this->getFunctionName($node);
if ($fn_name === 'Str\\join') {
$type = await find_type_for_node_async($root, $items[1], $path);
if ($type === 'string') {
$argument_order = Vec\reverse($argument_order ?? vec[]);
}
} else if ($fn_name === 'Str\\replace' || $fn_name === 'Str\\replace_ci') {
// str_replace and str_ireplace have two modes:
// string for search/replace args means replacing a single pattern
// arrays mean replacing a set of patterns, which we should rewrite as Str\replace_every
$type = await find_type_for_node_async($root, $items[0], $path);
if ($type !== 'string') {
if ($fn_name === 'Str\\replace_ci') {
// (note there is no Str\replace_every_ci at the moment, so this case is unhandled)
// bail to skip rewriting this call
return tuple(null, $found_namespaces);
}
// add Dict to set of required namespaces so we can call Dict\associate()
$found_namespaces[] = HslNamespace::DICT;
$node = $this->replaceFunctionName($node, 'Str\\replace_every');
$search_arg = $items[0]->getCode();
$replace_arg = $items[1]->getCode();
// replacement dictionary uses keys from first arg, values from second arg
$expr = 'Dict\\associate('.$search_arg.', '.$replace_arg.')';
$replacement_patterns = $this->expressionFromCode($expr);
$new_argument_list = new NodeList(vec[
new ListItem(
$items[2],
new CommaToken(null, new NodeList(vec[new WhiteSpace(' ')])),
),
new ListItem($replacement_patterns, null),
]);
return tuple(
$node->replace($argument_list, $new_argument_list),
$found_namespaces,
);
}
} else if ($fn_name === 'Str\\slice' && C\count($items) === 3) {
// check for negative length arguments to Str\slice, which will throw a runtime exception
$length = $this->resolveIntegerArgument($items[2]);
if ($length !== null && $length < 0) {
$offset = $this->resolveIntegerArgument($items[1]);
if ($offset === null) {
// skip this one if we don't have a sensible offset
return tuple(null, $found_namespaces);
}
// if the offset is negative too, it's pretty simple
// we can compute the correct length as abs(offset) + length and rewrite teh node
if ($offset < 0) {
$rewrite_length_value = Math\abs($offset) + $length;
$new_length = new ListItem(
new LiteralExpression(new DecimalLiteralToken(
null,
null,
(string)$rewrite_length_value,
)),
null,
);
} else {
// with a positive offset this is harder
// we need to replace this arg with a more complex expression
// based on the length of the string
$haystack = $items[0]->getCode();
$new_length = $this->expressionFromCode(
'Str\\length('.$haystack.') - '.($offset + Math\abs($length)),
);
}
// rewrite args list
$new_argument_list = $argument_list->replace($items[2], $new_length);
}
} else if ($fn_name === 'Str\\splice' && C\count($items) === 4) {
// check for negative length arguments to Str\splice, which will throw a runtime exception
// this is currently unhandled, so we just bail by returning null if we find it
$length = $this->resolveIntegerArgument($items[3]);
if ($length !== null && $length < 0) {
return tuple(null, $found_namespaces);
}
} else if (
$fn_name is nonnull &&
($fn_name === 'Math\\max' || $fn_name === 'Math\\min') &&
C\count($items) !== 1
) {
// PHP max() and min() either take a list of variadic args, or an array of args
// in HSL, max and min want a single Traversable arg, while maxva and minva are variadic
return tuple(
$this->replaceFunctionName($node, $fn_name.'va'),
$found_namespaces,
);
} else if ($fn_name === 'Math\\round' && C\count($items) > 2) {
// can't handle the optional third argument of round()
return tuple(null, $found_namespaces);
}
if ($argument_order !== null) {
$new_items = vec[];
foreach ($argument_order as $index) {
$new_items[] = $items[$index];
}
$new_argument_list = vec[];
foreach ($arguments as $i => $argument) {
$new_argument_list[] = $argument->replace(
$argument->getItemx(),
$new_items[$i],
);
}
$new_argument_list = new NodeList($new_argument_list);
}
return tuple(
$node->replace($argument_list, $new_argument_list),
$found_namespaces,
);
}