Class: SyntaxTree::CallChainFormatter

Inherits:
Object
  • Object
show all
Defined in:
lib/syntax_tree/node.rb

Overview

This is probably the most complicated formatter in this file. It’s responsible for formatting chains of method calls, with or without arguments or blocks. In general, we want to go from something like

foo.bar.baz

to

foo
  .bar
  .baz

Of course there are a lot of caveats to that, including trailing operators when necessary, where comments are places, how blocks are aligned, etc.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(node) ⇒ CallChainFormatter



2443
2444
2445
# File 'lib/syntax_tree/node.rb', line 2443

def initialize(node)
  @node = node
end

Instance Attribute Details

#nodeObject (readonly)

Call | MethodAddBlock

the top of the call chain



2441
2442
2443
# File 'lib/syntax_tree/node.rb', line 2441

def node
  @node
end

Class Method Details

.chained?(node) ⇒ Boolean



2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
# File 'lib/syntax_tree/node.rb', line 2581

def self.chained?(node)
  return false if ENV["STREE_FAST_FORMAT"]

  case node
  when Call
    true
  when MethodAddBlock
    node.call.is_a?(Call)
  else
    false
  end
end

Instance Method Details

#format(q) ⇒ Object



2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
# File 'lib/syntax_tree/node.rb', line 2447

def format(q)
  children = [node]
  threshold = 3

  # First, walk down the chain until we get to the point where we're not
  # longer at a chainable node.
  loop do
    case (child = children.last)
    when Call
      case (receiver = child.receiver)
      when Call
        children << receiver
      when MethodAddBlock
        receiver.call.is_a?(Call) ? children << receiver : break
      else
        break
      end
    when MethodAddBlock
      child.call.is_a?(Call) ? children << child.call : break
    else
      break
    end
  end

  # Here, we have very specialized behavior where if we're within a sig
  # block, then we're going to assume we're creating a Sorbet type
  # signature. In that case, we really want the threshold to be lowered so
  # that we create method chains off of any two method calls within the
  # block. For more details, see
  # https://github.com/prettier/plugin-ruby/issues/863.
  parents = q.parents.take(4)
  if (parent = parents[2])
    # If we're at a do_block, then we want to go one more level up. This is
    # because do blocks have BodyStmt nodes instead of just Statements
    # nodes.
    parent = parents[3] if parent.is_a?(DoBlock)

    if parent.is_a?(MethodAddBlock) && parent.call.is_a?(FCall) &&
         parent.call.value.value == "sig"
      threshold = 2
    end
  end

  if children.length >= threshold
    q.group do
      q
        .if_break { format_chain(q, children) }
        .if_flat { node.format_contents(q) }
    end
  else
    node.format_contents(q)
  end
end

#format_chain(q, children) ⇒ Object



2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
# File 'lib/syntax_tree/node.rb', line 2501

def format_chain(q, children)
  # We're going to have some specialized behavior for if it's an entire
  # chain of calls without arguments except for the last one. This is common
  # enough in Ruby source code that it's worth the extra complexity here.
  empty_except_last =
    children
      .drop(1)
      .all? { |child| child.is_a?(Call) && child.arguments.nil? }

  # Here, we're going to add all of the children onto the stack of the
  # formatter so it's as if we had descending normally into them. This is
  # necessary so they can check their parents as normal.
  q.stack.concat(children)
  q.format(children.last.receiver)

  q.group do
    if attach_directly?(children.last)
      format_child(q, children.pop)
      q.stack.pop
    end

    q.indent do
      # We track another variable that checks if you need to move the
      # operator to the previous line in case there are trailing comments
      # and a trailing operator.
      skip_operator = false

      while (child = children.pop)
        if child.is_a?(Call)
          if child.receiver.is_a?(Call) &&
               (child.receiver.message != :call) &&
               (child.receiver.message.value == "where") &&
               (child.message.value == "not")
            # This is very specialized behavior wherein we group
            # .where.not calls together because it looks better. For more
            # information, see
            # https://github.com/prettier/plugin-ruby/issues/862.
          else
            # If we're at a Call node and not a MethodAddBlock node in the
            # chain then we're going to add a newline so it indents
            # properly.
            q.breakable_empty
          end
        end

        format_child(
          q,
          child,
          skip_comments: children.empty?,
          skip_operator: skip_operator,
          skip_attached: empty_except_last && children.empty?
        )

        # If the parent call node has a comment on the message then we need
        # to print the operator trailing in order to keep it working.
        last_child = children.last
        if last_child.is_a?(Call) && last_child.message.comments.any?
          q.format(CallOperatorFormatter.new(last_child.operator))
          skip_operator = true
        else
          skip_operator = false
        end

        # Pop off the formatter's stack so that it aligns with what would
        # have happened if we had been formatting normally.
        q.stack.pop
      end
    end
  end

  if empty_except_last
    case node
    when Call
      node.format_arguments(q)
    when MethodAddBlock
      q.format(node.block)
    end
  end
end