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

Returns a new instance of CallChainFormatter.



2725
2726
2727
# File 'lib/syntax_tree/node.rb', line 2725

def initialize(node)
  @node = node
end

Instance Attribute Details

#nodeObject (readonly)

CallNode | MethodAddBlock

the top of the call chain



2723
2724
2725
# File 'lib/syntax_tree/node.rb', line 2723

def node
  @node
end

Class Method Details

.chained?(node) ⇒ Boolean

Returns:

  • (Boolean)


2876
2877
2878
2879
2880
2881
2882
2883
2884
2885
2886
2887
2888
# File 'lib/syntax_tree/node.rb', line 2876

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

  case node
  when CallNode
    !node.receiver.nil?
  when MethodAddBlock
    call = node.call
    call.is_a?(CallNode) && !call.receiver.nil?
  else
    false
  end
end

Instance Method Details

#format(q) ⇒ Object



2729
2730
2731
2732
2733
2734
2735
2736
2737
2738
2739
2740
2741
2742
2743
2744
2745
2746
2747
2748
2749
2750
2751
2752
2753
2754
2755
2756
2757
2758
2759
2760
2761
2762
2763
2764
2765
2766
2767
2768
2769
2770
2771
2772
2773
2774
2775
2776
2777
2778
2779
2780
2781
2782
2783
2784
2785
2786
2787
2788
2789
2790
2791
2792
2793
# File 'lib/syntax_tree/node.rb', line 2729

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 CallNode
      case (receiver = child.receiver)
      when CallNode
        if receiver.receiver.nil?
          break
        else
          children << receiver
        end
      when MethodAddBlock
        if (call = receiver.call).is_a?(CallNode) && !call.receiver.nil?
          children << receiver
        else
          break
        end
      else
        break
      end
    when MethodAddBlock
      if (call = child.call).is_a?(CallNode) && !call.receiver.nil?
        children << call
      else
        break
      end
    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 block with the `do` keywords, 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?(BlockNode) && parent.keywords?

    if parent.is_a?(MethodAddBlock) &&
         (call = parent.call).is_a?(CallNode) && call.message.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



2795
2796
2797
2798
2799
2800
2801
2802
2803
2804
2805
2806
2807
2808
2809
2810
2811
2812
2813
2814
2815
2816
2817
2818
2819
2820
2821
2822
2823
2824
2825
2826
2827
2828
2829
2830
2831
2832
2833
2834
2835
2836
2837
2838
2839
2840
2841
2842
2843
2844
2845
2846
2847
2848
2849
2850
2851
2852
2853
2854
2855
2856
2857
2858
2859
2860
2861
2862
2863
2864
2865
2866
2867
2868
2869
2870
2871
2872
2873
2874
# File 'lib/syntax_tree/node.rb', line 2795

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?(CallNode) && 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) if 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?(CallNode)
          if (receiver = child.receiver).is_a?(CallNode) &&
               (receiver.message != :call) &&
               (receiver.message.value == "where") &&
               (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?(CallNode) && last_child.message.comments.any? &&
             last_child.operator
          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 CallNode
      node.format_arguments(q)
    when MethodAddBlock
      q.format(node.block)
    end
  end
end