code: def check_split_join_balance(graph):
      msg0 = (
          "Step *end* reached before a split started at step(s) *{roots}* "
          "were joined. Add a join step before *end*."
      )
      msg1 = (
          "Step *{0.name}* seems like a join step (it takes an extra input "
          "argument) but an incorrect number of steps (*{paths}*) lead to "
          "it. This join was expecting {num_roots} incoming paths, starting "
          "from split step(s) *{roots}*."
      )
      msg2 = (
          "Step *{0.name}* seems like a join step (it takes an extra input "
          "argument) but it is not preceded by a split. Ensure that there is "
          "a matching split for every join."
      )
      msg3 = (
          "Step *{0.name}* joins steps from unrelated splits. Ensure that "
          "there is a matching join for every split."
      )
  
      def traverse(node, split_stack):
          if node.type in ("start", "linear"):
              new_stack = split_stack
          elif node.type in ("split", "foreach"):
              new_stack = split_stack + [("split", node.out_funcs)]
          elif node.type == "end":
              if split_stack:
                  _, split_roots = split_stack.pop()
                  roots = ", ".join(split_roots)
                  raise LintWarn(msg0.format(roots=roots))
          elif node.type == "join":
              if split_stack:
                  _, split_roots = split_stack[-1]
                  new_stack = split_stack[:-1]
                  if len(node.in_funcs) != len(split_roots):
                      paths = ", ".join(node.in_funcs)
                      roots = ", ".join(split_roots)
                      raise LintWarn(
                          msg1.format(
                              node,
                              paths=paths,
                              num_roots=len(split_roots),
                              roots=roots,
                          ),
                          node.func_lineno,
                      )
              else:
                  raise LintWarn(msg2.format(node), node.func_lineno)
  
              # check that incoming steps come from the same lineage
              # (no cross joins)
              def parents(n):
                  if graph[n].type == "join":
                      return tuple(graph[n].split_parents[:-1])
                  else:
                      return tuple(graph[n].split_parents)
  
              if not all_equal(map(parents, node.in_funcs)):
                  raise LintWarn(msg3.format(node), node.func_lineno)
  
          for n in node.out_funcs:
              traverse(graph[n], new_stack)
  
      traverse(graph["start"], [])
  
imports: import re
  from .exception import MetaflowException
  from .util import all_equal
label_desc: Use image node_id 11 for calling a global function with example usage: check_split_join_balance(graph) and returns: tuple, tuple