class RubyNext::Language::Rewriters::PatternMatching

Constants

ALTERNATION_MARKER
CURRENT_HASH_KEY
MATCHEE
MATCHEE_ARR
MATCHEE_HASH
MIN_SUPPORTED_VERSION
NAME
SYNTAX_PROBE

Attributes

deconstructed_keys[R]
predicates[R]

Public Instance Methods

on_case_match(node) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 241
def on_case_match(node)
  context.track! self

  @deconstructed_keys = {}
  @predicates = Predicates::CaseIn.new

  matchee_ast =
    s(:begin, s(:lvasgn, MATCHEE, node.children[0]))

  patterns = locals.with(
    matchee: MATCHEE,
    arr: MATCHEE_ARR,
    hash: MATCHEE_HASH
  ) do
    build_case_when(node.children[1..-1])
  end

  case_clause = predicates.process(s(:case, *patterns))

  rewrite_case_in! node, matchee_ast, case_clause

  node.updated(
    :kwbegin,
    [
      matchee_ast, case_clause
    ]
  )
end
on_in_match(node)
Alias for: on_match_pattern
on_match_pattern(node) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 270
def on_match_pattern(node)
  context.track! self

  @deconstructed_keys = {}
  @predicates = Predicates::Noop.new

  matchee =
    s(:begin, s(:lvasgn, MATCHEE, node.children[0]))

  pattern =
    locals.with(
      matchee: MATCHEE,
      arr: MATCHEE_ARR,
      hash: MATCHEE_HASH
    ) do
      send(
        :"#{node.children[1].type}_clause",
        node.children[1]
      ).then do |node|
        s(:begin,
          s(:or,
            node,
            no_matching_pattern))
      end
    end

  node.updated(
    :and,
    [
      matchee,
      pattern
    ]
  ).tap do |new_node|
    replace(node.loc.expression, inline_blocks(unparse(new_node)))
  end
end
Also aliased as: on_in_match

Private Instance Methods

arr_item_at(index, arr = s(:lvar, locals[:arr])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 650
def arr_item_at(index, arr = s(:lvar, locals[:arr]))
  s(:index, arr, index.to_ast_node)
end
arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 654
def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
  s(:index,
    arr,
    s(:irange,
      lindex.to_ast_node,
      rindex.to_ast_node))
end
array_element(index, head, *tail) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 494
def array_element(index, head, *tail)
  return array_match_rest(index, head, *tail) if head.type == :match_rest

  send("#{head.type}_array_element", head, index).then do |node|
    next node if tail.empty?

    s(:begin,
      s(:and,
        node,
        array_element(index + 1, *tail)))
  end
end
array_find(head, *nodes, tail) click to toggle source
*a, 1, 2, *

-> arr.find.with_index { |_, i| (a = arr.take(i)) && arr == 1 && arr[i + 1] == 2 }

# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 508
def array_find(head, *nodes, tail)
  index = s(:lvar, :__i__)

  match_vars = []

  head_match =
    unless head.children.empty?
      match_vars << s(:lvasgn, head.children[0].children[0])

      arr_take = s(:send,
        s(:lvar, locals[:arr]),
        :take,
        index)

      match_var_clause(head.children[0], arr_take)
    end

  tail_match =
    unless tail.children.empty?
      match_vars << s(:lvasgn, tail.children[0].children[0])

      match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
    end

  nodes.each do |node|
    if node.type == :match_var
      match_vars << s(:lvasgn, node.children[0])
    elsif node.type == :match_as
      match_vars << s(:lvasgn, node.children[1].children[0])
    end
  end

  pattern = array_rest_element(*nodes, index).then do |needle|
    next needle unless head_match
    s(:begin,
      s(:and,
        needle,
        head_match))
  end.then do |headed_needle|
    next headed_needle unless tail_match

    s(:begin,
      s(:and,
        headed_needle,
        tail_match))
  end

  s(:block,
    s(:send,
      s(:send,
        s(:lvar, locals[:arr]),
        :find),
      :with_index),
    s(:args,
      s(:arg, :_),
      s(:arg, :__i__)),
    pattern).then do |block|
    next block if match_vars.empty?

    # We need to declare match vars outside of `find` block
    locals_declare = s(:begin, s(:masgn,
      s(:mlhs, *match_vars),
      s(:nil)))

    s(:begin,
      s(:or,
        locals_declare,
        block))
  end
end
array_match_rest(index, node, *tail) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 579
def array_match_rest(index, node, *tail)
  size = tail.size + 1
  child = node.children[0]

  rest = arr_slice(index, -size).then do |r|
    next r unless child

    match_var_clause(
      child,
      r
    )
  end

  return rest if tail.empty?

  s(:begin,
    s(:and,
      rest,
      array_rest_element(*tail, -(size - 1))))
end
array_pattern_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 611
def array_pattern_array_element(node, index)
  element = arr_item_at(index)
  locals.with(arr: locals[:arr, index]) do
    predicates.push :"i#{index}"
    array_pattern_clause(node, element).tap { predicates.pop }
  end
end
array_pattern_clause(node, matchee = s(:lvar, locals[:matchee])) click to toggle source
ARRAY PATTERN (START) ===============
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 433
def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
  deconstruct_node(matchee).then do |dnode|
    size_check = nil
    # if there is no rest or tail, match the size first
    unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
      size_check = predicates.array_size(
        s(:begin,
          s(:send,
            node.children.size.to_ast_node,
            :==,
            s(:send, s(:lvar, locals[:arr]), :size))),
        node.children.size
      )
    end

    right =
      if node.children.empty?
        case_eq_clause(s(:array), s(:lvar, locals[:arr]))
      elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest
        array_find(*node.children)
      else
        array_element(0, *node.children)
      end

    right = s(:and, size_check, right) if size_check

    s(:begin,
      s(:and,
        dnode,
        right))
  end
end
array_pattern_hash_element(node, key) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 790
def array_pattern_hash_element(node, key)
  element = hash_value_at(key)
  key_index = deconstructed_key(key)
  locals.with(arr: locals[:hash, key_index]) do
    predicates.push :"k#{key_index}"
    array_pattern_clause(node, element).tap { predicates.pop }
  end
end
array_pattern_with_tail_clause(node, matchee = s(:lvar, locals[:matchee]))
array_rest_element(head, *tail, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 600
def array_rest_element(head, *tail, index)
  send("#{head.type}_array_element", head, index).then do |node|
    next node if tail.empty?

    s(:begin,
      s(:and,
        node,
        array_rest_element(*tail, index + 1)))
  end
end
build_case_when(nodes) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 342
def build_case_when(nodes)
  else_clause = nil
  clauses = []

  nodes.each do |clause|
    if clause&.type == :in_pattern
      clauses << build_when_clause(clause)
    else
      else_clause = process(clause)
    end
  end

  else_clause = (else_clause || no_matching_pattern).then do |node|
    next node unless node.type == :empty_else
    s(:empty)
  end

  clauses << else_clause
  clauses
end
build_when_clause(clause) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 363
def build_when_clause(clause)
  predicates.reset!
  [
    with_guard(
      send(
        :"#{clause.children[0].type}_clause",
        clause.children[0]
      ),
      clause.children[1] # guard
    ),
    process(clause.children[2] || s(:nil)) # expression
  ].then do |children|
    s(:when, *children)
  end
end
case_eq_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 646
def case_eq_array_element(node, index)
  case_eq_clause(node, arr_item_at(index))
end
case_eq_clause(node, right = s(:lvar, locals[:matchee])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 425
def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
  predicates.terminate!
  s(:begin, s(:send,
    process(node), :===, right))
end
case_eq_hash_element(node, key) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 864
def case_eq_hash_element(node, key)
  case_eq_clause node, hash_value_at(key)
end
check_match_var_alternation!(name) click to toggle source

Raise SyntaxError if match-var is used within alternation github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900

# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 952
def check_match_var_alternation!(name)
  return unless locals.key?(ALTERNATION_MARKER)

  return if name.start_with?("_")

  raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
end
const_pattern_clause(node, right = s(:lvar, locals[:matchee])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 379
def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
  const, pattern = *node.children

  predicates.const(case_eq_clause(const, right), const).then do |node|
    next node if pattern.nil?

    s(:begin,
      s(:and,
        node,
        send(:"#{pattern.type}_clause", pattern)))
  end
end
deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 730
def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
  # Use original hash returned by #deconstruct_keys if not **rest matching,
  # 'cause it remains immutable
  deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]

  # Duplicate the source hash when matching **rest, 'cause we mutate it
  hash_dup =
    if @hash_match_rest
      s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup)))
    else
      s(:true)
    end

  context.use_ruby_next!

  respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
  respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))

  key_names = keys.children.map { |node| node.children.last }
  predicates.push locals[:hash]

  s(:begin, s(:lvasgn, deconstruct_name,
    s(:send,
      matchee, :deconstruct_keys, keys))).then do |dnode|
    next dnode if respond_to_checked

    s(:and,
      respond_check,
      s(:begin,
        s(:and,
          s(:begin,
            s(:or,
              dnode,
              s(:true))),
          s(:begin,
            s(:or,
              s(:send,
                s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
              raise_error(:TypeError, "#deconstruct_keys must return Hash"))))))
  end.then do |dnode|
    predicates.hash_deconstructed(dnode, key_names)
  end.then do |dnode|
    next dnode unless @hash_match_rest

    s(:begin,
      s(:and,
        dnode,
        hash_dup))
  end
end
deconstruct_node(matchee) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 469
def deconstruct_node(matchee)
  context.use_ruby_next!

  # we do not memoize respond_to_check for arrays, 'cause
  # we can memoize is together with #deconstruct result
  respond_check = respond_to_check(matchee, :deconstruct)
  right = s(:send, matchee, :deconstruct)

  predicates.array_deconstructed(
    s(:and,
      respond_check,
      s(:begin,
        s(:and,
          s(:begin,
            s(:or,
              s(:begin, s(:lvasgn, locals[:arr], right)),
              s(:true))),
          s(:begin,
            s(:or,
              s(:send,
                s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
              raise_error(:TypeError, "#deconstruct must return Array"))))))
  )
end
deconstructed_key(key) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 960
def deconstructed_key(key)
  return deconstructed_keys[key] if deconstructed_keys.key?(key)

  deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
end
find_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
hash_element(head, *tail) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 799
def hash_element(head, *tail)
  send("#{head.type}_hash_element", head).then do |node|
    next node if tail.empty?

    right = hash_element(*tail)

    next node if right.nil?

    s(:begin,
      s(:and,
        node,
        right))
  end
end
hash_has_key(key, hash = s(:lvar, locals[:hash])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 882
def hash_has_key(key, hash = s(:lvar, locals[:hash]))
  s(:send,
    hash, :key?,
    key.to_ast_node)
end
hash_pattern_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 619
def hash_pattern_array_element(node, index)
  element = arr_item_at(index)
  locals.with(hash: locals[:arr, index]) do
    predicates.push :"i#{index}"
    hash_pattern_clause(node, element).tap { predicates.pop }
  end
end
hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee])) click to toggle source
HASH PATTERN (START) ===============
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 666
def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
  # Optimization: avoid hash modifications when not needed
  # (we use #dup and #delete when "reading" values when **rest is present
  # to assign the rest of the hash copy to it)
  @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
  keys = hash_pattern_destruction_keys(node.children)

  specified_key_names = hash_pattern_keys(node.children)

  deconstruct_keys_node(keys, matchee).then do |dnode|
    right =
      if node.children.empty?
        case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
      elsif specified_key_names.empty?
        hash_element(*node.children)
      else
        s(:begin,
          s(:and,
            having_hash_keys(specified_key_names),
            hash_element(*node.children)))
      end

    predicates.pop

    next dnode if right.nil?

    s(:begin,
      s(:and,
        dnode,
        right))
  end
end
hash_pattern_destruction_keys(children) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 708
def hash_pattern_destruction_keys(children)
  return s(:nil) if children.empty?

  children.filter_map do |child|
    # Skip ** without var
    next if child.type == :match_rest && child.children.empty?
    return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern

    send("#{child.type}_hash_key", child)
  end.then { |keys| s(:array, *keys) }
end
hash_pattern_hash_element(node, key) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 781
def hash_pattern_hash_element(node, key)
  element = hash_value_at(key)
  key_index = deconstructed_key(key)
  locals.with(hash: locals[:hash, key_index]) do
    predicates.push :"k#{key_index}"
    hash_pattern_clause(node, element).tap { predicates.pop }
  end
end
hash_pattern_keys(children) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 699
def hash_pattern_keys(children)
  children.filter_map do |child|
    # Skip ** without var
    next if child.type == :match_rest || child.type == :match_nil_pattern

    send("#{child.type}_hash_key", child)
  end
end
hash_value_at(key, hash = s(:lvar, locals[:hash])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 868
def hash_value_at(key, hash = s(:lvar, locals[:hash]))
  return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)

  if @hash_match_rest
    s(:send,
      hash, :delete,
      key.to_ast_node)
  else
    s(:index,
      hash,
      key.to_ast_node)
  end
end
having_hash_keys(keys, hash = s(:lvar, locals[:hash])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 888
def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
  keys.reduce(nil) do |acc, key|
    pnode = hash_has_key(key, hash)
    next pnode unless acc

    s(:begin,
      s(:and, acc, pnode))
  end.then do |node|
    predicates.hash_keys(node, keys)
  end
end
inline_blocks(source) click to toggle source

Unparser generates `do .. end` or `{ … }` multiline blocks, we want to have single-line blocks with `{ … }`.

# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 968
def inline_blocks(source)
  source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }')
end
match_alt_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 627
def match_alt_array_element(node, index)
  children = node.children.map do |child, i|
    send :"#{child.type}_array_element", child, index
  end
  s(:begin, s(:or, *children))
end
match_alt_clause(node) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 392
def match_alt_clause(node)
  children = locals.with(ALTERNATION_MARKER => true) do
    node.children.map.with_index do |child, i|
      predicates.terminate! if i == 1
      send :"#{child.type}_clause", child
    end
  end
  s(:begin, s(:or, *children))
end
match_alt_hash_element(node, key) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 819
def match_alt_hash_element(node, key)
  element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key)))

  children = locals.with(hash_element: locals[:hash, :el]) do
    node.children.map do |child, i|
      send :"#{child.type}_hash_element", child, key
    end
  end

  s(:begin,
    s(:and,
      s(:begin,
        s(:or,
          element_node,
          s(:true))),
      s(:begin,
        s(:or, *children))))
end
match_as_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 638
def match_as_array_element(node, index)
  match_as_clause(node, arr_item_at(index))
end
match_as_clause(node, right = s(:lvar, locals[:matchee])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 402
def match_as_clause(node, right = s(:lvar, locals[:matchee]))
  s(:begin,
    s(:and,
      send(:"#{node.children[0].type}_clause", node.children[0], right),
      match_var_clause(node.children[1], right)))
end
match_as_hash_element(node, key) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 838
def match_as_hash_element(node, key)
  match_as_clause(node, hash_value_at(key))
end
match_nil_pattern_hash_element(node, _key = nil) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 847
def match_nil_pattern_hash_element(node, _key = nil)
  s(:send,
    s(:lvar, locals[:hash]),
    :empty?)
end
match_rest_hash_element(node, _key = nil) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 853
def match_rest_hash_element(node, _key = nil)
  # case {}; in **; end
  return if node.children.empty?

  child = node.children[0]

  raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var

  match_var_clause(child, s(:lvar, locals[:hash]))
end
match_var_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 634
def match_var_array_element(node, index)
  match_var_clause(node, arr_item_at(index))
end
match_var_clause(node, left = s(:lvar, locals[:matchee])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 409
def match_var_clause(node, left = s(:lvar, locals[:matchee]))
  return s(:true) if node.children[0] == :_

  check_match_var_alternation! node.children[0]

  s(:begin,
    s(:or,
      s(:begin, s(:lvasgn, node.children[0], left)),
      s(:true)))
end
match_var_hash_element(node, key = nil) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 842
def match_var_hash_element(node, key = nil)
  key ||= node.children[0]
  match_var_clause(node, hash_value_at(key))
end
match_var_hash_key(node) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 724
def match_var_hash_key(node)
  check_match_var_alternation! node.children[0]

  s(:sym, node.children[0])
end
method_missing(mid, *args, &block) click to toggle source
Calls superclass method
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 938
def method_missing(mid, *args, &block)
  mid = mid.to_s
  return case_eq_clause(*args) if mid.match?(/_clause$/)
  return case_eq_array_element(*args) if mid.match?(/_array_element$/)
  return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
  super
end
no_matching_pattern() click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 914
def no_matching_pattern
  raise_error(
    :NoMatchingPatternError,
    s(:send,
      s(:lvar, locals[:matchee]), :inspect)
  )
end
pair_hash_element(node, _key = nil) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 814
def pair_hash_element(node, _key = nil)
  key, val = *node.children
  send("#{val.type}_hash_element", val, key)
end
pair_hash_key(node) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 720
def pair_hash_key(node)
  node.children[0]
end
pin_array_element(node, index) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 642
def pin_array_element(node, index)
  case_eq_array_element node.children[0], index
end
pin_clause(node, right = s(:lvar, locals[:matchee])) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 420
def pin_clause(node, right = s(:lvar, locals[:matchee]))
  predicates.terminate!
  case_eq_clause node.children[0], right
end
raise_error(type, msg = "") click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 922
def raise_error(type, msg = "")
  s(:send, s(:const, nil, :Kernel), :raise,
    s(:const, nil, type),
    msg.to_ast_node)
end
respond_to_check(node, mid) click to toggle source

Add respond_to? check

# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 929
def respond_to_check(node, mid)
  s(:send, node, :respond_to?, mid.to_ast_node)
end
respond_to_missing?(mid, *) click to toggle source
Calls superclass method
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 933
def respond_to_missing?(mid, *)
  return true if mid.to_s.match?(/_(clause|array_element)/)
  super
end
rewrite_case_in!(node, matchee, new_node) click to toggle source
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 311
def rewrite_case_in!(node, matchee, new_node)
  replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false")
  remove(node.children[0].loc.expression)

  node.children[1..-1].each.with_index do |clause, i|
    if clause&.type == :in_pattern
      # handle multiline clauses differently
      if clause.loc.last_line > clause.children[0].loc.last_line + 1
        height = clause.loc.last_line - clause.children[0].loc.last_line
        padding = "\n" * height
        body_indent = " " * clause.children[2].loc.column
        replace(
          clause.loc.expression,
          "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \
          "#{padding}" \
          "#{body_indent}#{clause.children[2].loc.expression.source}"
        )
      else
        replace(
          clause.loc.keyword.end.join(clause.children[0].loc.expression.end),
          inline_blocks(unparse(new_node.children[i].children[0]))
        )
        remove(clause.children[1].loc.expression) if clause.children[1]
        replace(clause.loc.keyword, "when ")
      end
    elsif clause.nil?
      insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}")
    end
  end
end
with_guard(node, guard) click to toggle source
HASH PATTERN (END) ===============
# File lib/ruby-next/language/rewriters/pattern_matching.rb, line 902
def with_guard(node, guard)
  return node unless guard

  s(:begin,
    s(:and,
      node,
      guard.children[0])).then do |expr|
    next expr unless guard.type == :unless_guard
    s(:send, expr, :!)
  end
end