lib/opal/nodes/iter.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

require 'opal/nodes/node_with_args'

module Opal
  module Nodes
    class IterNode < NodeWithArgs
      handle :iter

      children :inline_args, :stmts

      def compile
        is_lambda! if scope.lambda_definition?

        compile_body_or_shortcut

        blockopts = {}
        blockopts["$$arity"] = arity if arity < 0
        blockopts["$$s"] = scope.self if @define_self
        blockopts["$$brk"] = @closure.throwers[:break] if @closure&.throwers&.key? :break
        blockopts["$$ret"] = @closure.throwers[:return] if @closure&.throwers&.key? :return

        if compiler.arity_check?
          blockopts["$$parameters"] = parameters_code
        end

        if compiler.enable_source_location?
          blockopts["$$source_location"] = source_location
        end

        # MRI expands a passed argument if the block:
        # 1. takes a single argument that is an array
        # 2. has more that one argument
        # With a few exceptions:
        # 1. mlhs arg: if a block takes |(a, b)| argument
        # 2. trailing ',' in the arg list (|a, |)
        # This flag on the method indicates that a block has a top level mlhs argument
        # which means that we have to expand passed array explicitly in runtime.
        if has_top_level_mlhs_arg?
          blockopts["$$has_top_level_mlhs_arg"] = "true"
        end

        if has_trailing_comma_in_args?
          blockopts["$$has_trailing_comma_in_args"] = "true"
        end

        if blockopts.keys == ["$$arity"]
          push ", #{arity}"
        elsif !blockopts.empty?
          push ', {', blockopts.map { |k, v| "#{k}: #{v}" }.join(', '), '}'
        end

        scope.nesting if @define_nesting
        scope.relative_access if @define_relative_access
      end

      def compile_body
        inline_params = nil

        to_vars = identity = body_code = nil

        in_scope do
          identity = scope.identify!

          inline_params = process(inline_args)

          compile_arity_check

          in_closure(Closure::JS_FUNCTION | Closure::ITER | (@is_lambda ? Closure::LAMBDA : 0)) do
            body_code = stmt(returned_body)

            add_temp "self = #{identity}.$$s == null ? this : #{identity}.$$s" if @define_self

            to_vars = scope.to_vars

            line body_code
          end
        end

        unshift to_vars

        if await_encountered
          unshift "async function #{identity}(", inline_params, '){'
        else
          unshift "function #{identity}(", inline_params, '){'
        end
        push '}'
      end

      def compile_block_arg
        if block_arg
          scope.prepare_block
        end
      end

      def extract_underscore_args
        valid_args = []
        caught_blank_argument = false

        args.children.each do |arg|
          arg_name = arg.children.first
          if arg_name == :_
            unless caught_blank_argument
              caught_blank_argument = true
              valid_args << arg
            end
          else
            valid_args << arg
          end
        end

        @sexp = @sexp.updated(
          nil, [
            args.updated(nil, valid_args),
            stmts
          ]
        )
      end

      def returned_body
        compiler.returns(stmts || s(:nil))
      end

      def has_top_level_mlhs_arg?
        original_args.children.any? { |arg| arg.type == :mlhs }
      end

      def has_trailing_comma_in_args?
        if original_args.loc && original_args.loc.expression
          args_source = original_args.loc.expression.source
          args_source.match(/,\s*\|/)
        end
      end

      def arity_check_node
        s(:iter_arity_check, original_args)
      end
    end
  end
end