# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2021 The Elixir Team

defmodule Code.Fragment do
  @moduledoc """
  This module provides conveniences for analyzing fragments of
  textual code and extract available information whenever possible.

  This module should be considered experimental.
  """

  @type position :: {line :: pos_integer(), column :: pos_integer()}

  @typedoc """
  Options for cursor context functions.

  Currently, these options are not used but reserved for future extensibility.
  """
  @type cursor_opts :: []

  @typedoc """
  Options for converting code fragments to quoted expressions.
  """
  @type container_cursor_to_quoted_opts :: [
          file: String.t(),
          line: pos_integer(),
          column: pos_integer(),
          columns: boolean(),
          token_metadata: boolean(),
          literal_encoder: (term(), Macro.metadata() -> term()),
          trailing_fragment: String.t()
        ]

  @doc ~S"""
  Returns the list of lines in the given string, preserving their line endings.

  Only the line endings recognized by the Elixir compiler are
  considered, namely `\r\n` and `\n`. If you would like the retrieve
  lines without their line endings, use `String.split(string, ["\r\n", "\n"])`.

  ## Examples

      iex> Code.Fragment.lines("foo\r\nbar\r\nbaz")
      ["foo\r\n", "bar\r\n", "baz"]

      iex> Code.Fragment.lines("foo\nbar\nbaz")
      ["foo\n", "bar\n", "baz"]

      iex> Code.Fragment.lines("")
      [""]

  """
  @doc since: "1.19.0"
  def lines(string) do
    lines(string, <<>>)
  end

  defp lines(<<?\n, rest::binary>>, acc),
    do: [<<acc::binary, ?\n>> | lines(rest, <<>>)]

  defp lines(<<char, rest::binary>>, acc),
    do: lines(rest, <<acc::binary, char>>)

  defp lines(<<>>, acc),
    do: [acc]

  @doc """
  Receives a string and returns the cursor context.

  This function receives a string with an Elixir code fragment,
  representing a cursor position, and based on the string, it
  provides contextual information about the latest token.
  The return of this function can then be used to provide tips,
  suggestions, and autocompletion functionality.

  This function performs its analyses on tokens. This means
  it does not understand how constructs are nested within each
  other. See the "Limitations" section below.

  Consider adding a catch-all clause when handling the return
  type of this function as new cursor information may be added
  in future releases.

  ## Examples

      iex> Code.Fragment.cursor_context("")
      :expr

      iex> Code.Fragment.cursor_context("hello_wor")
      {:local_or_var, ~c"hello_wor"}

  ## Return values

    * `{:alias, charlist}` - the context is an alias, potentially
      a nested one, such as `Hello.Wor` or `HelloWor`

    * `{:alias, inside_alias, charlist}` - the context is an alias, potentially
      a nested one, where `inside_alias` is an expression `{:module_attribute, charlist}`
      or `{:local_or_var, charlist}` and `charlist` is a static part
      Examples are `__MODULE__.Submodule` or `@hello.Submodule`

    * `{:block_keyword_or_binary_operator, charlist}` - may be a block keyword (do, end, after,
      catch, else, rescue) or a binary operator

    * `{:dot, inside_dot, charlist}` - the context is a dot
      where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
      `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
      itself. If a var is given, this may either be a remote call or a map
      field access. Examples are `Hello.wor`, `:hello.wor`, `hello.wor`,
      `Hello.nested.wor`, `hello.nested.wor`, and `@hello.world`. If `charlist`
      is empty and `inside_dot` is an alias, then the autocompletion may either
      be an alias or a remote call.

    * `{:dot_arity, inside_dot, charlist}` - the context is a dot arity
      where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
      `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
      itself. If a var is given, it must be a remote arity. Examples are
      `Hello.world/`, `:hello.world/`, `hello.world/2`, and `@hello.world/2`

    * `{:dot_call, inside_dot, charlist}` - the context is a dot
      call. This means parentheses or space have been added after the expression.
      where `inside_dot` is either a `{:var, charlist}`, `{:alias, charlist}`,
      `{:module_attribute, charlist}`, `{:unquoted_atom, charlist}` or a `dot`
      itself. If a var is given, it must be a remote call. Examples are
      `Hello.world(`, `:hello.world(`, `Hello.world `, `hello.world(`, `hello.world `,
      and `@hello.world(`

    * `:expr` - may be any expression. Autocompletion may suggest an alias,
      local or var

    * `{:local_or_var, charlist}` - the context is a variable or a local
      (import or local) call, such as `hello_wor`

    * `{:local_arity, charlist}` - the context is a local (import or local)
      arity, such as `hello_world/`

    * `{:local_call, charlist}` - the context is a local (import or local)
      call, such as `hello_world(` and `hello_world `

    * `{:anonymous_call, inside_caller}` - the context is an anonymous
      call, such as `fun.(` and `@fun.(`.

    * `{:module_attribute, charlist}` - the context is a module attribute,
      such as `@hello_wor`

    * `{:operator, charlist}` - the context is an operator, such as `+` or
      `==`. Note textual operators, such as `when` do not appear as operators
      but rather as `:local_or_var`. `@` is never an `:operator` and always a
      `:module_attribute`

    * `{:operator_arity, charlist}` - the context is an operator arity, which
      is an operator followed by /, such as `+/`, `not/` or `when/`

    * `{:operator_call, charlist}` - the context is an operator call, which is
      an operator followed by space, such as `left + `, `not ` or `x when `

    * `:none` - no context possible

    * `{:sigil, charlist}` - the context is a sigil. It may be either the beginning
      of a sigil, such as `~` or `~s`, or an operator starting with `~`, such as
      `~>` and `~>>`

    * `{:struct, inside_struct}` - the context is a struct, such as `%`, `%UR` or `%URI`.
      `inside_struct` can either be a `charlist` in case of a static alias or an
      expression `{:alias, inside_alias, charlist}`, `{:module_attribute, charlist}`,
      `{:local_or_var, charlist}`, `{:dot, inside_dot, charlist}`

    * `{:unquoted_atom, charlist}` - the context is an unquoted atom. This
      can be any atom or an atom representing a module

  We recommend looking at the test suite of this function for a complete list
  of examples and their return values.

  ## Limitations

  The analysis is based on the current token, by analysing the last line of
  the input. For example, this code:

      iex> Code.Fragment.cursor_context("%URI{")
      :expr

  returns `:expr`, which suggests any variable, local function or alias
  could be used. However, given we are inside a struct, the best suggestion
  would be a struct field. In such cases, you can use
  `container_cursor_to_quoted`, which will return the container of the AST
  the cursor is currently within. You can then analyse this AST to provide
  completion of field names.

  As a consequence of its token-based implementation, this function considers
  only the last line of the input. This means it will show suggestions inside
  strings, heredocs, etc, which is intentional as it helps with doctests,
  references, and more.
  """
  @doc since: "1.13.0"
  @spec cursor_context(List.Chars.t(), cursor_opts()) ::
          {:alias, charlist}
          | {:alias, inside_alias, charlist}
          | {:block_keyword_or_binary_operator, charlist}
          | {:dot, inside_dot, charlist}
          | {:dot_arity, inside_dot, charlist}
          | {:dot_call, inside_dot, charlist}
          | :expr
          | {:local_or_var, charlist}
          | {:local_arity, charlist}
          | {:local_call, charlist}
          | {:anonymous_call, inside_caller}
          | {:module_attribute, charlist}
          | {:operator, charlist}
          | {:operator_arity, charlist}
          | {:operator_call, charlist}
          | :none
          | {:sigil, charlist}
          | {:struct, inside_struct}
          | {:unquoted_atom, charlist}
        when inside_dot:
               {:alias, charlist}
               | {:alias, inside_alias, charlist}
               | {:dot, inside_dot, charlist}
               | {:module_attribute, charlist}
               | {:unquoted_atom, charlist}
               | {:var, charlist}
               | :expr,
             inside_alias:
               {:local_or_var, charlist}
               | {:module_attribute, charlist},
             inside_struct:
               charlist
               | {:alias, inside_alias, charlist}
               | {:local_or_var, charlist}
               | {:module_attribute, charlist}
               | {:dot, inside_dot, charlist},
             inside_caller: {:var, charlist} | {:module_attribute, charlist}
  def cursor_context(fragment, opts \\ [])

  def cursor_context(fragment, opts)
      when (is_binary(fragment) or is_list(fragment)) and is_list(opts) do
    fragment
    |> last_line()
    |> :lists.reverse()
    |> codepoint_cursor_context(opts)
    |> elem(0)
  end

  def cursor_context(other, opts) when is_list(opts) do
    cursor_context(to_charlist(other), opts)
  end

  @operators ~c"\\<>+-*/:=|&~^%!$"
  @starting_punctuation ~c",([{;"
  @closing_punctuation ~c")]}\"'"
  @space ~c"\t\s"
  @trailing_identifier ~c"?!"
  @tilde_op_prefix ~c"<=~"

  @non_identifier @trailing_identifier ++
                    @operators ++ @starting_punctuation ++ @closing_punctuation ++ @space ++ [?.]

  @textual_operators ~w(when not and or in)c
  @keywords ~w(do end after else catch rescue fn true false nil)c

  defp codepoint_cursor_context(reverse, _opts) do
    {stripped, spaces} = strip_spaces(reverse, 0)

    case stripped do
      # It is empty
      [] -> {:expr, 0}
      # Structs
      [?%, ?:, ?: | _] -> {{:struct, ~c""}, 1}
      [?%, ?: | _] -> {{:unquoted_atom, ~c"%"}, 2}
      [?% | _] -> {{:struct, ~c""}, 1}
      # Token/AST only operators
      [?>, ?= | rest] when rest == [] or hd(rest) != ?: -> {:expr, 0}
      [?>, ?- | rest] when rest == [] or hd(rest) != ?: -> {:expr, 0}
      # Two-digit containers
      [?<, ?< | rest] when rest == [] or hd(rest) != ?< -> {:expr, 0}
      # Ambiguity around :
      [?: | rest] when rest == [] or hd(rest) != ?: -> unquoted_atom_or_expr(spaces)
      # Dots
      [?.] -> {:none, 0}
      [?. | rest] when hd(rest) not in ~c".:" -> dot(rest, spaces + 1, ~c"")
      # It is a local or remote call with parens
      [?( | rest] -> call_to_cursor_context(strip_spaces(rest, spaces + 1))
      # A local arity definition
      [?/ | rest] -> arity_to_cursor_context(strip_spaces(rest, spaces + 1))
      # Starting a new expression
      [h | _] when h in @starting_punctuation -> {:expr, 0}
      # It is keyword, binary operator, a local or remote call without parens
      rest when spaces > 0 -> closing_or_call_to_cursor_context({rest, spaces})
      # It is an identifier
      _ -> identifier_to_cursor_context(reverse, spaces, false)
    end
  end

  defp strip_spaces([h | rest], count) when h in @space, do: strip_spaces(rest, count + 1)
  defp strip_spaces(rest, count), do: {rest, count}

  defp unquoted_atom_or_expr(0), do: {{:unquoted_atom, ~c""}, 1}
  defp unquoted_atom_or_expr(_), do: {:expr, 0}

  defp arity_to_cursor_context({reverse, spaces}) do
    case identifier_to_cursor_context(reverse, spaces, true) do
      {{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
      {{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
      {{:operator, acc}, count} -> {{:operator_arity, acc}, count}
      {{:sigil, _}, _} -> {:none, 0}
      {_, _} -> {{:operator, ~c"/"}, 1}
    end
  end

  defp call_to_cursor_context({reverse, spaces}) do
    with [?. | rest] <- reverse,
         {rest, spaces} = strip_spaces(rest, spaces),
         [h | _] when h not in @non_identifier <- rest do
      case identifier_to_cursor_context(rest, spaces, true) do
        {{:local_or_var, acc}, count} -> {{:anonymous_call, {:var, acc}}, count + 1}
        {{:module_attribute, _} = attr, count} -> {{:anonymous_call, attr}, count + 1}
        {_, _} -> {:none, 0}
      end
    else
      _ ->
        case identifier_to_cursor_context(reverse, spaces, true) do
          {{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
          {{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
          {{:operator, acc}, count} -> {{:operator_call, acc}, count}
          {_, _} -> {:none, 0}
        end
    end
  end

  defp closing_or_call_to_cursor_context({reverse, spaces}) do
    if closing?(reverse) do
      {{:block_keyword_or_binary_operator, ~c""}, 0}
    else
      call_to_cursor_context({reverse, spaces})
    end
  end

  defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
  defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
  defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
  defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}

  defp identifier_to_cursor_context(reverse, count, call_op?) do
    case identifier(reverse, count) do
      :none ->
        {:none, 0}

      :operator ->
        operator(reverse, count, [], call_op?)

      {:struct, {:module_attribute, acc}, count} ->
        {{:struct, {:module_attribute, acc}}, count + 1}

      {:module_attribute, acc, count} ->
        {{:module_attribute, acc}, count}

      {:sigil, acc, count} ->
        {{:sigil, acc}, count}

      {:unquoted_atom, acc, count} ->
        {{:unquoted_atom, acc}, count}

      {:alias, rest, acc, count} ->
        case strip_spaces(rest, count) do
          {~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
            nested_alias(rest, count + 1, acc)

          {~c"%" ++ _, count} ->
            {{:struct, acc}, count + 1}

          _ ->
            {{:alias, acc}, count}
        end

      {:identifier, _, acc, count} when call_op? and acc in @textual_operators ->
        {{:operator, acc}, count}

      {:identifier, [?%], acc, count} ->
        case identifier_to_cursor_context(acc |> Enum.reverse(), count, true) do
          {{:local_or_var, _} = identifier, _} -> {{:struct, identifier}, count + 1}
          _ -> {:none, 0}
        end

      {:identifier, rest, acc, count} ->
        case strip_spaces(rest, count) do
          {~c"." ++ rest, count} when rest == [] or hd(rest) != ?. ->
            dot(rest, count + 1, acc)

          {rest, rest_count} ->
            response =
              if rest_count > count and closing?(rest),
                do: :block_keyword_or_binary_operator,
                else: :local_or_var

            {{response, acc}, count}
        end

      {:capture_arg, acc, count} ->
        {{:capture_arg, acc}, count}
    end
  end

  # If it is a closing punctuation
  defp closing?([h | _]) when h in @closing_punctuation, do: true
  # Closing bitstring (but deal with operators)
  defp closing?([?>, ?> | rest]), do: rest == [] or hd(rest) not in [?>, ?~]
  # Keywords
  defp closing?(rest) do
    case split_non_identifier(rest, []) do
      {~c"nil", _} -> true
      {~c"true", _} -> true
      {~c"false", _} -> true
      {[digit | _], _} when digit in ?0..?9 -> true
      {[upper | _], _} when upper in ?A..?Z -> true
      {[_ | _], [?: | rest]} -> rest == [] or hd(rest) != ?:
      {_, _} -> false
    end
  end

  defp split_non_identifier([h | t], acc) when h not in @non_identifier,
    do: split_non_identifier(t, [h | acc])

  defp split_non_identifier(rest, acc), do: {acc, rest}

  defp identifier([?? | rest], count), do: check_identifier(rest, count + 1, [??])
  defp identifier([?! | rest], count), do: check_identifier(rest, count + 1, [?!])
  defp identifier(rest, count), do: check_identifier(rest, count, [])

  defp check_identifier([h | t], count, acc) when h not in @non_identifier,
    do: rest_identifier(t, count + 1, [h | acc])

  defp check_identifier(_, _, _), do: :operator

  defp rest_identifier([h | rest], count, acc) when h not in @non_identifier do
    rest_identifier(rest, count + 1, [h | acc])
  end

  defp rest_identifier(rest, count, [?@ | acc]) do
    case tokenize_identifier(rest, count, acc) do
      {:identifier, [?% | _rest], acc, count} -> {:struct, {:module_attribute, acc}, count}
      {:identifier, _rest, acc, count} -> {:module_attribute, acc, count}
      :none when acc == [] -> {:module_attribute, ~c"", count}
      _ -> :none
    end
  end

  defp rest_identifier([?~ | rest], count, [letter])
       when (letter in ?A..?Z or letter in ?a..?z) and
              (rest == [] or hd(rest) not in @tilde_op_prefix) do
    {:sigil, [letter], count + 1}
  end

  defp rest_identifier([?: | rest], count, acc) when rest == [] or hd(rest) != ?: do
    case String.Tokenizer.tokenize(acc) do
      {_, _, [], _, _, _} -> {:unquoted_atom, acc, count + 1}
      _ -> :none
    end
  end

  defp rest_identifier([?? | _], _count, _acc) do
    :none
  end

  defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
    if Enum.all?(acc, &(&1 in ?0..?9)) do
      {:capture_arg, [?& | acc], count + 1}
    else
      tokenize_identifier(rest, count, acc)
    end
  end

  defp rest_identifier(rest, count, acc) do
    tokenize_identifier(rest, count, acc)
  end

  defp tokenize_identifier(rest, count, acc) do
    case String.Tokenizer.tokenize(acc) do
      # Not actually an atom cause rest is not a :
      {:atom, _, _, _, _, _} ->
        :none

      # Aliases must be ascii only
      {:alias, _, _, _, false, _} ->
        :none

      {kind, _, [], _, _, extra} ->
        if :at in extra do
          :none
        else
          {kind, rest, acc, count}
        end

      _ ->
        :none
    end
  end

  defp nested_alias(rest, count, acc) do
    {rest, count} = strip_spaces(rest, count)

    case identifier_to_cursor_context(rest, count, true) do
      {{:struct, prev}, count} when is_list(prev) ->
        {{:struct, prev ++ ~c"." ++ acc}, count}

      {{:struct, {:alias, parent, prev}}, count} ->
        {{:struct, {:alias, parent, prev ++ ~c"." ++ acc}}, count}

      {{:struct, prev}, count} ->
        {{:struct, {:alias, prev, acc}}, count}

      {{:alias, prev}, count} ->
        {{:alias, prev ++ ~c"." ++ acc}, count}

      {{:alias, parent, prev}, count} ->
        {{:alias, parent, prev ++ ~c"." ++ acc}, count}

      {{:local_or_var, prev}, count} ->
        {{:alias, {:local_or_var, prev}, acc}, count}

      {{:module_attribute, prev}, count} ->
        {{:alias, {:module_attribute, prev}, acc}, count}

      _ ->
        {:none, 0}
    end
  end

  defp dot(rest, count, acc) do
    {rest, count} = strip_spaces(rest, count)

    case identifier_to_cursor_context(rest, count, true) do
      {{:local_or_var, var}, count} ->
        {{:dot, {:var, var}, acc}, count}

      {{:unquoted_atom, _} = prev, count} ->
        {{:dot, prev, acc}, count}

      {{:alias, _} = prev, count} ->
        {{:dot, prev, acc}, count}

      {{:alias, _, _} = prev, count} ->
        {{:dot, prev, acc}, count}

      {{:struct, inner}, count} when is_list(inner) ->
        {{:struct, {:dot, {:alias, inner}, acc}}, count}

      {{:struct, inner}, count} ->
        {{:struct, {:dot, inner, acc}}, count}

      {{:dot, _, _} = prev, count} ->
        {{:dot, prev, acc}, count}

      {{:module_attribute, _} = prev, count} ->
        {{:dot, prev, acc}, count}

      {:expr, count} ->
        {{:dot, :expr, acc}, count}

      {_, _} ->
        {:none, 0}
    end
  end

  defp operator([h | rest], count, acc, call_op?) when h in @operators do
    operator(rest, count + 1, [h | acc], call_op?)
  end

  # If we are opening a sigil, ignore the operator.
  defp operator([letter, ?~ | rest], _count, [op], _call_op?)
       when op in ~c"<|/" and (letter in ?A..?Z or letter in ?a..?z) and
              (rest == [] or hd(rest) not in @tilde_op_prefix) do
    {:none, 0}
  end

  defp operator(rest, count, ~c"~", call_op?) do
    {rest, _} = strip_spaces(rest, count)

    if call_op? or match?([?. | rest] when rest == [] or hd(rest) != ?., rest) do
      {:none, 0}
    else
      {{:sigil, ~c""}, count}
    end
  end

  defp operator([?) | rest], _, [], true) when hd(rest) != ?? do
    {:expr, 0}
  end

  defp operator(rest, count, acc, _call_op?) do
    case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
      {:ok, _, _, _, [{:atom, _, _}], []} ->
        {{:unquoted_atom, tl(acc)}, count}

      {:ok, _, _, _, [{_, _, op}], []} ->
        {rest, dot_count} = strip_spaces(rest, count)

        cond do
          Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
            {:none, 0}

          match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
            dot(tl(rest), dot_count + 1, acc)

          true ->
            {{:operator, acc}, count}
        end

      _ ->
        {:none, 0}
    end
  end

  @doc """
  Receives a string and returns the surround context.

  This function receives a string with an Elixir code fragment
  and a `position`. It returns a map containing the beginning
  and ending of the identifier alongside its context, or `:none`
  if there is nothing with a known context. This is useful to
  provide mouse-over and highlight functionality in editors.

  The difference between `cursor_context/2` and `surround_context/3`
  is that the former assumes the expression in the code fragment
  is incomplete. For example, `do` in `cursor_context/2` may be
  a keyword or a variable or a local call, while `surround_context/3`
  assumes the expression in the code fragment is complete, therefore
  `do` would always be a keyword.

  The `position` contains both the `line` and `column`, both starting
  with the index of 1. The column must precede the surrounding expression.
  For example, the expression `foo`, will return something for the columns
  1, 2, and 3, but not 4:

      foo
      ^ column 1

      foo
       ^ column 2

      foo
        ^ column 3

      foo
         ^ column 4

  The returned map contains the column the expression starts and the
  first column after the expression ends.

  Similar to `cursor_context/2`, this function is also token-based
  and may not be accurate under all circumstances. See the
  "Return values" and "Limitations" section under `cursor_context/2`
  for more information.

  ## Examples

      iex> Code.Fragment.surround_context("foo", {1, 1})
      %{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}

  ## Differences to `cursor_context/2`

  Because `surround_context/3` attempts to capture complex expressions,
  it has some differences to `cursor_context/2`:

    * `dot_call`/`dot_arity` and `operator_call`/`operator_arity`
      are collapsed into `dot` and `operator` contexts respectively
      as there aren't any meaningful distinctions between them

    * On the other hand, this function still makes a distinction between
      `local_call`/`local_arity` and `local_or_var`, since the latter can
      be a local or variable

    * `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
      (in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`

    * This function never returns empty sigils `{:sigil, ~c""}` or empty structs
      `{:struct, ~c""}` as context

    * This function returns keywords as `{:keyword, ~c"do"}`

    * This function never returns `:expr`

  We recommend looking at the test suite of this function for a complete list
  of examples and their return values.
  """
  @doc since: "1.13.0"
  @spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
          %{begin: position, end: position, context: context} | :none
        when context:
               {:alias, charlist}
               | {:alias, inside_alias, charlist}
               | {:dot, inside_dot, charlist}
               | {:local_or_var, charlist}
               | {:local_arity, charlist}
               | {:local_call, charlist}
               | {:module_attribute, charlist}
               | {:operator, charlist}
               | {:sigil, charlist}
               | {:struct, inside_struct}
               | {:unquoted_atom, charlist}
               | {:keyword, charlist}
               | {:key, charlist}
               | {:capture_arg, charlist},
             inside_dot:
               {:alias, charlist}
               | {:alias, inside_alias, charlist}
               | {:dot, inside_dot, charlist}
               | {:module_attribute, charlist}
               | {:unquoted_atom, charlist}
               | {:var, charlist}
               | :expr,
             inside_alias:
               {:local_or_var, charlist}
               | {:module_attribute, charlist},
             inside_struct:
               charlist
               | {:alias, inside_alias, charlist}
               | {:local_or_var, charlist}
               | {:module_attribute, charlist}
               | {:dot, inside_dot, charlist}
  def surround_context(fragment, position, options \\ [])

  def surround_context(string, {line, column}, opts)
      when (is_binary(string) or is_list(string)) and is_list(opts) do
    {charlist, lines_before_lengths, lines_current_and_after_lengths} =
      surround_line(string, line, column)

    prepended_columns = Enum.sum(lines_before_lengths)

    charlist
    |> position_surround_context(line, column + prepended_columns, opts)
    |> to_multiline_range(
      prepended_columns,
      lines_before_lengths,
      lines_current_and_after_lengths
    )
  end

  def surround_context(other, {_, _} = position, opts) do
    surround_context(to_charlist(other), position, opts)
  end

  defp position_surround_context(charlist, line, column, opts)
       when is_integer(line) and line >= 1 and is_integer(column) and column >= 1 do
    {reversed_pre, post} = string_reverse_at(charlist, column - 1, [])
    {reversed_pre, post} = adjust_position(reversed_pre, post)

    case take_identifier(post, []) do
      {_, [], _} ->
        maybe_operator(reversed_pre, post, line, opts)

      {:identifier, reversed_post, rest} ->
        {keyword_key?, rest} =
          case rest do
            [?: | tail] when tail == [] or hd(tail) in @space ->
              {true, rest}

            _ ->
              {rest, _} = strip_spaces(rest, 0)
              {false, rest}
          end

        reversed = reversed_post ++ reversed_pre

        case codepoint_cursor_context(reversed, opts) do
          {{:struct, acc}, offset} ->
            build_surround({:struct, acc}, reversed, line, offset)

          {{:alias, acc}, offset} ->
            build_surround({:alias, acc}, reversed, line, offset)

          {{:alias, parent, acc}, offset} ->
            build_surround({:alias, parent, acc}, reversed, line, offset)

          {{:dot, _, [_ | _]} = dot, offset} ->
            build_surround(dot, reversed, line, offset)

          {{:local_or_var, acc}, offset} when keyword_key? ->
            build_surround({:key, acc}, reversed, line, offset)

          {{:local_or_var, acc}, offset} when hd(rest) == ?( ->
            build_surround({:local_call, acc}, reversed, line, offset)

          {{:local_or_var, acc}, offset} when hd(rest) == ?/ ->
            build_surround({:local_arity, acc}, reversed, line, offset)

          {{:local_or_var, acc}, offset} when acc in @textual_operators ->
            build_surround({:operator, acc}, reversed, line, offset)

          {{:local_or_var, acc}, offset} when acc in @keywords ->
            build_surround({:keyword, acc}, reversed, line, offset)

          {{:local_or_var, acc}, offset} ->
            build_surround({:local_or_var, acc}, reversed, line, offset)

          {{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
            build_surround({:operator, acc}, reversed, line, offset)

          {{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
            build_surround({:keyword, acc}, reversed, line, offset)

          {{:module_attribute, ~c""}, offset} ->
            build_surround({:operator, ~c"@"}, reversed, line, offset)

          {{:module_attribute, acc}, offset} ->
            build_surround({:module_attribute, acc}, reversed, line, offset)

          {{:sigil, acc}, offset} ->
            build_surround({:sigil, acc}, reversed, line, offset)

          {{:unquoted_atom, acc}, offset} ->
            build_surround({:unquoted_atom, acc}, reversed, line, offset)

          {{:capture_arg, acc}, offset} ->
            build_surround({:capture_arg, acc}, reversed, line, offset)

          _ ->
            maybe_operator(reversed_pre, post, line, opts)
        end

      {:alias, reversed_post, _rest} ->
        reversed = reversed_post ++ reversed_pre

        case codepoint_cursor_context(reversed, opts) do
          {{:alias, acc}, offset} ->
            build_surround({:alias, acc}, reversed, line, offset)

          {{:alias, parent, acc}, offset} ->
            build_surround({:alias, parent, acc}, reversed, line, offset)

          {{:struct, acc}, offset} ->
            build_surround({:struct, acc}, reversed, line, offset)

          _ ->
            :none
        end
    end
  end

  defp maybe_operator(reversed_pre, post, line, opts) do
    case take_operator(post, []) do
      {[], _rest} ->
        :none

      {reversed_post, rest} ->
        reversed = reversed_post ++ reversed_pre

        case codepoint_cursor_context(reversed, opts) do
          {{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
            arg = Enum.take_while(rest, &(&1 in ?0..?9))

            build_surround(
              {:capture_arg, ~c"&" ++ arg},
              :lists.reverse(arg, reversed),
              line,
              offset + length(arg)
            )

          {{:operator, acc}, offset} ->
            build_surround({:operator, acc}, reversed, line, offset)

          {{:sigil, ~c""}, offset} when hd(rest) in ?A..?Z or hd(rest) in ?a..?z ->
            build_surround({:sigil, [hd(rest)]}, [hd(rest) | reversed], line, offset + 1)

          {{:dot, _, [_ | _]} = dot, offset} ->
            build_surround(dot, reversed, line, offset)

          _ ->
            :none
        end
    end
  end

  defp build_surround(context, reversed, line, offset) do
    {post, reversed_pre} = enum_reverse_at(reversed, offset, [])
    pre = :lists.reverse(reversed_pre)
    pre_length = :string.length(pre) + 1

    %{
      context: context,
      begin: {line, pre_length},
      end: {line, pre_length + :string.length(post)}
    }
  end

  defp take_identifier([h | t], acc) when h in @trailing_identifier,
    do: {:identifier, [h | acc], t}

  defp take_identifier([h | t], acc) when h not in @non_identifier,
    do: take_identifier(t, [h | acc])

  defp take_identifier(rest, acc) do
    with {[?. | t], _} <- strip_spaces(rest, 0),
         {[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
      take_alias(rest, acc)
    else
      _ -> {:identifier, acc, rest}
    end
  end

  defp take_alias([h | t], acc) when h in ?A..?Z or h in ?a..?z or h in ?0..?9 or h == ?_,
    do: take_alias(t, [h | acc])

  defp take_alias(rest, acc) do
    with {[?. | t], acc} <- move_spaces(rest, acc),
         {[h | t], acc} when h in ?A..?Z <- move_spaces(t, [?. | acc]) do
      take_alias(t, [h | acc])
    else
      _ -> {:alias, acc, rest}
    end
  end

  defp take_operator([h | t], acc) when h in @operators, do: take_operator(t, [h | acc])
  defp take_operator([h | t], acc) when h == ?., do: take_operator(t, [h | acc])
  defp take_operator(rest, acc), do: {acc, rest}

  # Unquoted atom handling
  defp adjust_position(reversed_pre, [?: | post])
       when hd(post) != ?: and (reversed_pre == [] or hd(reversed_pre) != ?:) do
    {[?: | reversed_pre], post}
  end

  defp adjust_position(reversed_pre, [?% | post]) do
    adjust_position([?% | reversed_pre], post)
  end

  # Dot/struct handling
  defp adjust_position(reversed_pre, post) do
    case move_spaces(post, reversed_pre) do
      # If we are between spaces and a dot, move past the dot
      {[?. | post], reversed_pre} when hd(post) != ?. and hd(reversed_pre) != ?. ->
        {post, reversed_pre} = move_spaces(post, [?. | reversed_pre])
        {reversed_pre, post}

      _ ->
        case strip_spaces(reversed_pre, 0) do
          # If there is a dot to our left, make sure to move to the first character
          {[?. | rest], _} when rest == [] or hd(rest) not in ~c".:" ->
            {post, reversed_pre} = move_spaces(post, reversed_pre)
            {reversed_pre, post}

          # If there is a % to our left, make sure to move to the first character
          {[?% | _], _} ->
            case move_spaces(post, reversed_pre) do
              {[h | _] = post, reversed_pre} when h in ?A..?Z ->
                {reversed_pre, post}

              _ ->
                {reversed_pre, post}
            end

          _ ->
            {reversed_pre, post}
        end
    end
  end

  defp move_spaces([h | t], acc) when h in @space, do: move_spaces(t, [h | acc])
  defp move_spaces(t, acc), do: {t, acc}

  defp string_reverse_at(charlist, 0, acc), do: {acc, charlist}

  defp string_reverse_at(charlist, n, acc) do
    case :unicode_util.gc(charlist) do
      [gc | cont] when is_integer(gc) -> string_reverse_at(cont, n - 1, [gc | acc])
      [gc | cont] when is_list(gc) -> string_reverse_at(cont, n - 1, :lists.reverse(gc, acc))
      [] -> {acc, []}
    end
  end

  defp enum_reverse_at([h | t], n, acc) when n > 0, do: enum_reverse_at(t, n - 1, [h | acc])
  defp enum_reverse_at(rest, _, acc), do: {acc, rest}

  defp last_line(binary) when is_binary(binary) do
    [last_line | lines_reverse] =
      binary
      |> String.split(["\r\n", "\n"])
      |> Enum.reverse()

    prepend_cursor_lines(lines_reverse, String.to_charlist(last_line))
  end

  defp last_line(charlist) when is_list(charlist) do
    [last_line | lines_reverse] =
      charlist
      |> :string.replace(~c"\r\n", ~c"\n", :all)
      |> :string.join(~c"")
      |> :string.split(~c"\n", :all)
      |> Enum.reverse()

    prepend_cursor_lines(lines_reverse, last_line)
  end

  defp prepend_cursor_lines(lines, last_line) do
    with [line | lines] <- lines,
         {trimmed_line, incomplete?} = ends_as_incomplete(to_charlist(line), [], true),
         true <- incomplete? or starts_with_dot?(last_line) do
      prepend_cursor_lines(lines, Enum.reverse(trimmed_line, last_line))
    else
      _ -> last_line
    end
  end

  defp starts_with_dot?([?. | _]), do: true
  defp starts_with_dot?([h | t]) when h in @space, do: starts_with_dot?(t)
  defp starts_with_dot?(_), do: false

  defp ends_as_incomplete([?# | _], acc, incomplete?),
    do: {acc, incomplete?}

  defp ends_as_incomplete([h | t], acc, _incomplete?) when h in [?(, ?.],
    do: ends_as_incomplete(t, [h | acc], true)

  defp ends_as_incomplete([h | t], acc, incomplete?) when h in @space,
    do: ends_as_incomplete(t, [h | acc], incomplete?)

  defp ends_as_incomplete([h | t], acc, _incomplete?),
    do: ends_as_incomplete(t, [h | acc], false)

  defp ends_as_incomplete([], acc, incomplete?),
    do: {acc, incomplete?}

  defp surround_line(binary, line, column) when is_binary(binary) do
    binary
    |> String.split(["\r\n", "\n"])
    |> Enum.map(&String.to_charlist/1)
    |> surround_lines(line, column)
  end

  defp surround_line(charlist, line, column) when is_list(charlist) do
    charlist
    |> :string.replace(~c"\r\n", ~c"\n", :all)
    |> :string.join(~c"")
    |> :string.split(~c"\n", :all)
    |> surround_lines(line, column)
  end

  defp surround_lines(lines, line, column) do
    {lines_before_reverse, cursor_line, lines_after} = split_at(lines, line, [])
    {trimmed_cursor_line, incomplete?} = ends_as_incomplete(to_charlist(cursor_line), [], true)

    reversed_cursor_line =
      if column - 1 > length(trimmed_cursor_line) do
        # Don't strip comments if cursor is inside a comment
        Enum.reverse(cursor_line)
      else
        trimmed_cursor_line
      end

    {cursor_line, after_lengths} =
      append_surround_lines(lines_after, [], [reversed_cursor_line], incomplete?)

    {cursor_line, before_lengths} = prepend_surround_lines(lines_before_reverse, [], cursor_line)
    {cursor_line, before_lengths, [length(reversed_cursor_line) | after_lengths]}
  end

  defp split_at([line], _, acc), do: {acc, line, []}
  defp split_at([line | lines], 1, acc), do: {acc, line, lines}
  defp split_at([line | lines], count, acc), do: split_at(lines, count - 1, [line | acc])

  defp prepend_surround_lines(lines, lengths, last_line) do
    with [line | lines] <- lines,
         {trimmed_line, incomplete?} = ends_as_incomplete(to_charlist(line), [], true),
         true <- incomplete? or starts_with_dot?(last_line) do
      lengths = [length(trimmed_line) | lengths]
      prepend_surround_lines(lines, lengths, Enum.reverse(trimmed_line, last_line))
    else
      _ -> {last_line, Enum.reverse(lengths)}
    end
  end

  defp append_surround_lines(lines, lengths, acc_lines, incomplete?) do
    with [line | lines] <- lines,
         line = to_charlist(line),
         true <- incomplete? or starts_with_dot?(line) do
      {trimmed_line, incomplete?} = ends_as_incomplete(line, [], true)
      lengths = [length(trimmed_line) | lengths]
      append_surround_lines(lines, lengths, [trimmed_line | acc_lines], incomplete?)
    else
      _ -> {Enum.reduce(acc_lines, [], &Enum.reverse/2), Enum.reverse(lengths)}
    end
  end

  defp to_multiline_range(:none, _, _, _), do: :none

  defp to_multiline_range(
         %{begin: {begin_line, begin_column}, end: {end_line, end_column}} = context,
         prepended,
         lines_before_lengths,
         lines_current_and_after_lengths
       ) do
    {begin_line, begin_column} =
      Enum.reduce_while(lines_before_lengths, {begin_line, begin_column - prepended}, fn
        line_length, {acc_line, acc_column} ->
          if acc_column < 1 do
            {:cont, {acc_line - 1, acc_column + line_length}}
          else
            {:halt, {acc_line, acc_column}}
          end
      end)

    {end_line, end_column} =
      Enum.reduce_while(lines_current_and_after_lengths, {end_line, end_column - prepended}, fn
        line_length, {acc_line, acc_column} ->
          if acc_column > line_length + 1 do
            {:cont, {acc_line + 1, acc_column - line_length}}
          else
            {:halt, {acc_line, acc_column}}
          end
      end)

    %{context | begin: {begin_line, begin_column}, end: {end_line, end_column}}
  end

  @doc """
  Receives a string and returns a quoted expression
  with the cursor AST position within its parent expression.

  This function receives a string with an Elixir code fragment,
  representing a cursor position, and converts such string to
  AST with the inclusion of special `__cursor__()` node representing
  the cursor position within its container (i.e. its parent).

  For example, take this code, which would be given as input:

      max(some_value,

  This function will return the AST equivalent to:

      max(some_value, __cursor__())

  In other words, this function is capable of closing any open
  brackets and insert the cursor position. Other content at the
  cursor position which is not a parent is discarded.
  For example, if this is given as input:

      max(some_value, another_val

  It will return the same AST:

      max(some_value, __cursor__())

  Similarly, if only this is given:

      max(some_va

  Then it returns:

      max(__cursor__())

  Calls without parenthesis are also supported, as we assume the
  brackets are implicit.

  Tuples, lists, maps, and binaries all retain the cursor position:

      max(some_value, [1, 2,

  Returns the following AST:

      max(some_value, [1, 2, __cursor__()])

  Keyword lists (and do-end blocks) are also retained. The following:

      if(some_value, do:
      if(some_value, do: :token
      if(some_value, do: 1 + val

  all return:

      if(some_value, do: __cursor__())

  For multi-line blocks, all previous lines are preserved.

  The AST returned by this function is not safe to evaluate but
  it can be analyzed and expanded.

  ## Examples

  Function call:

      iex> Code.Fragment.container_cursor_to_quoted("max(some_value, ")
      {:ok, {:max, [line: 1], [{:some_value, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}

  Containers (for example, a list):

      iex> Code.Fragment.container_cursor_to_quoted("[some, value")
      {:ok, [{:some, [line: 1], nil}, {:__cursor__, [line: 1], []}]}

  If an expression is complete, then the whole expression is discarded
  and only the parent is returned:

      iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var)")
      {:ok, {:if, [line: 1], [{:__cursor__, [line: 1], []}]}}

  this means complete expressions themselves return only the cursor:

      iex> Code.Fragment.container_cursor_to_quoted("if(is_atom(var))")
      {:ok, {:__cursor__, [line: 1], []}}

  Operators are also included from Elixir v1.15:

      iex> Code.Fragment.container_cursor_to_quoted("foo +")
      {:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}

  In order to parse the left-side of `->` properly, which appears both
  in anonymous functions and do-end blocks, the trailing fragment option
  must be given with the rest of the contents:

      iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
      {:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}

  ## Options

    * `:file` - the filename to be reported in case of parsing errors.
      Defaults to `"nofile"`.

    * `:line` - the starting line of the string being parsed.
      Defaults to `1`.

    * `:column` - the starting column of the string being parsed.
      Defaults to `1`.

    * `:columns` - when `true`, attach a `:column` key to the quoted
      metadata. Defaults to `false`.

    * `:token_metadata` - when `true`, includes token-related
      metadata in the expression AST, such as metadata for `do` and `end`
      tokens, for closing tokens, end of expressions, as well as delimiters
      for sigils. See `t:Macro.metadata/0`. Defaults to `false`.

    * `:literal_encoder` - a function to encode literals in the AST.
      See the documentation for `Code.string_to_quoted/2` for more information.

    * `:trailing_fragment` (since v1.18.0) - the rest of the contents after
      the cursor. This is necessary to correctly complete anonymous functions
      and the left-hand side of `->`

  """
  @doc since: "1.13.0"
  @spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
          {:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
  def container_cursor_to_quoted(fragment, opts \\ []) do
    {trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
    opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
    opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts

    file = Keyword.get(opts, :file, "nofile")
    line = Keyword.get(opts, :line, 1)
    column = Keyword.get(opts, :column, 1)

    case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
      {:ok, line, column, _warnings, rev_tokens, rev_terminators}
      when trailing_fragment == nil ->
        {rev_tokens, rev_terminators} =
          with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
               {_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
               true <- maybe_missing_stab?(rev_tokens, false),
               [_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
            {[close, open, cursor | rev_tokens], after_fn}
          else
            _ -> {rev_tokens, rev_terminators}
          end

        tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
        :elixir.tokens_to_quoted(tokens, file, opts)

      {:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
        tokens =
          with {before_start, [_ | _] = after_start} <-
                 Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
               true <- maybe_missing_stab?(rev_tokens, true),
               opts =
                 Keyword.put(opts, :check_terminators, {:cursor, before_start}),
               {:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
                 :elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
            trailing_tokens =
              reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)

            # If the cursor has its own line, then we do not trim new lines trailing tokens.
            # Otherwise we want to drop any newline so we drop the next tokens after eol.
            trailing_tokens =
              case rev_tokens do
                [_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
                _ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
              end

            Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
          else
            _ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
          end

        :elixir.tokens_to_quoted(tokens, file, opts)

      {:error, info, _rest, _warnings, _so_far} ->
        {:error, :elixir.format_token_error(info)}
    end
  end

  defp reverse_tokens(line, column, tokens, terminators) do
    {terminators, _} =
      Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
        atom = :elixir_tokenizer.terminator(start)

        {{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
      end)

    Enum.reverse(tokens, terminators)
  end

  # Otherwise we drop all tokens, trying to build a minimal AST
  # for cursor completion.
  defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
  defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens

  defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
  defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
  defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
  defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
  defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)

  defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
  defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
  defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
  defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
  defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
  defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)

  defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
  defp drop_tokens([], _counter), do: []

  defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
  defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
  defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
  defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
  defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
  defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
  defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
  defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
  defp maybe_missing_stab?([], _stab_choice?), do: false
end
