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.



2344
2345
2346
# File 'lib/syntax_tree/node.rb', line 2344

def initialize(node)
  @node = node
end

Instance Attribute Details

#nodeObject (readonly)

Call | MethodAddBlock

the top of the call chain



2342
2343
2344
# File 'lib/syntax_tree/node.rb', line 2342

def node
  @node
end

Class Method Details

.chained?(node) ⇒ Boolean

Returns:

  • (Boolean)


2477
2478
2479
2480
2481
2482
2483
2484
# File 'lib/syntax_tree/node.rb', line 2477

def self.chained?(node)
  case node
  in Call | MethodAddBlock[call: Call]
    true
  else
    false
  end
end

Instance Method Details

#format(q) ⇒ Object



2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
# File 'lib/syntax_tree/node.rb', line 2348

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 children.last
    in Call[receiver: Call]
      children << children.last.receiver
    in Call[receiver: MethodAddBlock[call: Call]]
      children << children.last.receiver
    in MethodAddBlock[call: Call]
      children << children.last.call
    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)

    case parent
    in MethodAddBlock[call: FCall[value: { value: "sig" }]]
      threshold = 2
    else
    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



2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
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
# File 'lib/syntax_tree/node.rb', line 2398

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)
        case child
        in Call[
             receiver: Call[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.
        in Call
          # 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("")
        else
        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.
        case children.last
        in Call[message: { comments: [_, *] }, operator:]
          q.format(CallOperatorFormatter.new(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
    in Call
      node.format_arguments(q)
    in MethodAddBlock[block:]
      q.format(block)
    end
  end
end