protected async function maybeMutateArgumentsAsync()

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,
    );
  }