Update Serum.Template.Storage

Update Serum.Template.Storage

- Fix spec of get/2
- Add put/3

defmodule Serum.Template.Storage do
  @moduledoc false

  _moduledocp = "An agent which stores compiled templates and includes."

  use Agent
  alias Serum.Result
  alias Serum.Template

  @initial_value %{template: %{}, include: %{}}

  @spec start_link(term()) :: Agent.on_start()
  def start_link(_) do
    Agent.start_link(fn -> @initial_value end, name: __MODULE__)
  end

  @spec load(Template.collection(), Template.type()) :: Result.t()
  def load(templates, type) when type in ~w(template include)a do
    Agent.update(__MODULE__, &Map.put(&1, type, templates))
  end

  @spec get(binary(), Template.type()) :: Template.t() | nil
  def get(name, type) do
    Agent.get(__MODULE__, &get_in(&1, [type, name]))
  end

  @spec put(binary(), Template.type(), Template.t()) :: Result.t()
  def put(name, type, template) do
    Agent.update(__MODULE__, &put_in(&1, [type, name], template))
  end

  @spec reset() :: :ok
  def reset, do: Agent.update(__MODULE__, fn _ -> @initial_value end)
end

defmodule Serum.Template.Storage do
  @moduledoc false

  _moduledocp = "An agent which stores compiled templates and includes."

  use Agent
  alias Serum.Result
  alias Serum.Template

  @initial_value %{template: %{}, include: %{}}

  @spec start_link(term()) :: Agent.on_start()
  def start_link(_) do
    Agent.start_link(fn -> @initial_value end, name: __MODULE__)
  end

  @spec load(Template.collection(), Template.type()) :: Result.t()
  def load(templates, type) when type in ~w(template include)a do
    Agent.update(__MODULE__, &Map.put(&1, type, templates))
  end

  @spec get(binary(), Template.type()) :: Result.t(Template.t())
  def get(name, type) do
    Agent.get(__MODULE__, &get_in(&1, [type, name]))
  end

  @spec reset() :: :ok
  def reset, do: Agent.update(__MODULE__, fn _ -> @initial_value end)
end


Improve error message for overriden deps

defrecord Mix.Dep, [ scm: nil, app: nil, requirement: nil, status: nil, opts: nil,
                     deps: [], extra: nil, manager: nil, from: nil ] do
  @moduledoc """
  This is a record that keeps information about your project
  dependencies. It contains:

  * `scm` - a module representing the source code management tool (SCM) operations;
  * `app` - the application name as an atom;
  * `requirement` - a binary or regex with the dependency's requirement
  * `status` - the current status of the dependency, check `Mix.Deps.format_status/1` for more info;
  * `opts` - the options given by the developer
  * `deps` - dependencies of this dependency
  * `manager` - the project management, possible values: `:rebar` | `:mix` | `:make` | `nil`
  * `from` - path to the file where the dependency was defined
  * `extra` - a slot for adding extra configuration based on the scm.
              the information on this field is private to the scm and
              should not be relied on.
  """
end

defmodule Mix.Deps do
  @moduledoc %S"""
  Common functions to work with dependencies.

  Dependencies must be specified in the Mix application in the
  following format:

      { app :: atom, opts :: Keyword.t }
      { app :: atom, requirement :: String.t, opts :: Keyword.t }

  The application name must be an atom, the version requirement must
  be a string according to the specification outline in `Version`
  and opts is a keyword lists that may include options for the underlying
  SCM or options used by Mix. Each set of options is documented below.

  Inside Mix, those dependencies are converted to a `Mix.Dep` record.
  This module provides conveniences to work with those dependencies
  and the dependencies are usually in two specific states: fetched and
  unfetched.

  When a dependency is unfetched, it means Mix only parsed its specification
  and made no attempt to actually fetch the dependency or validate its
  status. When the dependency is fetched, it means Mix attempted to fetch
  and validate it, the status is set in the status field.

  ## Mix options

  * `:app` - Do not try to read the app file for this dependency
  * `:env` - The environment to run the dependency on, defaults to :prod
  * `:compile` - A command to compile the dependency, defaults to a mix,
                 rebar or make command

  ## Git options (`:git`)

  * `:git`        - The git repository URI
  * `:github`     - A shortcut for specifying git repos from github, uses `git:`
  * `:ref`        - The reference to checkout (may be a branch, a commit sha or a tag)
  * `:branch`     - The git branch to checkout
  * `:tag`        - The git tag to checkout
  * `:submodules` - When true, initialize submodules for the repo

  ## Path options (`:path`)

  * `:path` - The path for the dependency
  * `:in_umbrella` - When true, sets a path dependency pointing to "../#{app}",
                     sharing the same environment as the current application

  ## Internal options

  Those options are set internally by Mix and they can't be
  overriden from the Mixfile:

  * `:dest` - The destination path for the dependency
  * `:lock` - The lock information retrieved from mix.lock

  """

  @doc """
  Returns all children dependencies for the current project,
  as well as the defined apps in case of umbrella projects.
  The children dependencies returned by this function were
  not fetched yet.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  defdelegate children(), to: Mix.Deps.Retriever

  @doc """
  Returns fetched dependencies recursively as a `Mix.Dep` record.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def fetched do
    { deps, _ } = Mix.Deps.Converger.all(nil, fn(dep, acc) -> { dep, acc } end)
    Mix.Deps.Converger.topsort(deps)
  end

  @doc """
  Receives a list of  dependency names and returns fetched dependency
  records. Logs a message if the dependency could not be found.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def fetched_by_name(given, all_deps // fetched) do
    # Ensure all apps are atoms
    apps = to_app_names(given)

    # We need to keep the order of all, which properly orders deps
    deps = Enum.filter all_deps, fn(dep) -> dep.app in apps end

    # Now we validate the given atoms
    index = Mix.Dep.__record__(:index, :app)
    Enum.each apps, fn(app) ->
      unless List.keyfind(deps, app, index) do
        Mix.shell.info "unknown dependency #{app} for env #{Mix.env}"
      end
    end

    deps
  end

  @doc """
  Maps and reduces over all unfetched dependencies, one by one.

  This is useful in case you want to retrieve the dependency
  tree for a project but process and change them along the way.
  For example, `mix deps.get` uses it to get all dependencies
  by first fetching the parent and then updating the tree as it goes.

  The callback expects the current dependency and the accumulator
  as arguments. The accumulator is returned as result.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def unfetched(acc, callback) do
    { _deps, acc } = Mix.Deps.Converger.all(acc, callback)
    acc
  end

  @doc """
  Receives a list of dependency names and maps and reduces over
  them.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def unfetched_by_name(given, acc, callback) do
    names = to_app_names(given)

    unfetched(acc, fn(dep, acc) ->
      if dep.app in names do
        callback.(dep, acc)
      else
        { dep, acc }
      end
    end)
  end

  @doc """
  Receives a list of dependencies and returns the given list
  with their depending dependencies, recursively. It is expected
  the given list of dependencies to contain only fetched dependencies
  (since unfetched dependencies did not have their dependencies
  retrieved yet).
  """
  def with_depending(deps, all_deps // fetched) do
    (deps ++ do_with_depending(deps, all_deps))
    |> Enum.uniq(&(&1.app))
  end

  defp do_with_depending([], _all_deps) do
    []
  end

  defp do_with_depending(deps, all_deps) do
    dep_names = Enum.map(deps, fn dep -> dep.app end)

    parents = Enum.filter all_deps, fn dep ->
      Enum.any?(dep.deps, fn child_dep -> child_dep.app in dep_names end)
    end

    do_with_depending(parents, all_deps) ++ parents
  end

  @doc """
  Runs the given `fun` inside the given dependency project by
  changing the current working directory and loading the given
  project onto the project stack.

  In case the project is a rebar dependency, a `Mix.Rebar` project
  will be pushed into the stack to simulate the rebar configuration.

  It is expected a fetched dependency as argument.
  """
  def in_dependency(dep, post_config // [], fun)

  def in_dependency(Mix.Dep[manager: :rebar, opts: opts], post_config, fun) do
    # Use post_config for rebar deps
    Mix.Project.post_config(post_config)
    Mix.Project.push(Mix.Rebar)
    try do
      File.cd!(opts[:dest], fn -> fun.(Mix.Rebar) end)
    after
      Mix.Project.pop
    end
  end

  def in_dependency(Mix.Dep[app: app, opts: opts], post_config, fun) do
    env     = opts[:env] || :prod
    old_env = Mix.env

    try do
      Mix.env(env)
      Mix.Project.in_project(app, opts[:dest], post_config, fun)
    after
      Mix.env(old_env)
    end
  end

  @doc """
  Formats the status of a dependency.
  """
  def format_status(Mix.Dep[status: { :ok, _vsn }]),
    do: "ok"

  def format_status(Mix.Dep[status: { :noappfile, path }]),
    do: "could not find an app file at #{Path.relative_to_cwd(path)}, " <>
        "this may happen when you specified the wrong application name in your deps " <>
        "or if the dependency failed to compile (which can be amended with `mix deps.compile`)"

  def format_status(Mix.Dep[status: { :invalidapp, path }]),
    do: "the app file at #{Path.relative_to_cwd(path)} is invalid"

  def format_status(Mix.Dep[status: { :invalidvsn, vsn }]),
    do: "the app file contains an invalid version: #{inspect vsn}"

  def format_status(Mix.Dep[status: { :nomatchvsn, vsn }, requirement: req]),
    do: "the dependency does not match the requirement #{inspect req}, got #{inspect vsn}"

  def format_status(Mix.Dep[status: { :lockmismatch, _ }]),
    do: "lock mismatch: the dependency is out of date"

  def format_status(Mix.Dep[status: :lockoutdated]),
    do: "lock outdated: the lock is outdated compared to the options in your mixfile"

  def format_status(Mix.Dep[status: :nolock]),
    do: "the dependency is not locked"

  def format_status(Mix.Dep[app: app, status: { :diverged, other }] = dep) do
    "different specs were given for the #{inspect app} app:\n" <>
    "#{dep_status(dep)}#{dep_status(other)}" <>
    "\n  Ensure they match or specify one in your #{inspect Mix.Project.get} deps and set `override: true`"
  end

  def format_status(Mix.Dep[app: app, status: { :overriden, other }] = dep) do
    "the dependency #{app} in #{Path.relative_to_cwd(dep.from)} is overriding a child dependency:\n" <>
    "#{dep_status(dep)}#{dep_status(other)}" <>
    "\n  Ensure they match or specify one in your #{inspect Mix.Project.get} deps and set `override: true`"
  end

  def format_status(Mix.Dep[status: { :unavailable, _ }]),
    do: "the dependency is not available, run `mix deps.get`"

  def format_status(Mix.Dep[status: { :elixirlock, _ }]),
    do: "the dependency is built with an out-of-date elixir version, run `mix deps.get`"

  defp dep_status(Mix.Dep[app: app, requirement: req, opts: opts, from: from]) do
    info = { app, req, Dict.drop(opts, [:dest, :lock, :env]) }
    "\n  > In #{Path.relative_to_cwd(from)}:\n    #{inspect info}\n"
  end

  @doc """
  Checks the lock for the given dependency and update its status accordingly.
  """
  def check_lock(Mix.Dep[scm: scm, app: app, opts: opts] = dep, lock) do
    if available?(dep) do
      rev  = lock[app]
      opts = Keyword.put(opts, :lock, rev)

      case scm.lock_status(opts) do
        :mismatch ->
          dep.status(if rev, do: { :lockmismatch, rev }, else: :nolock)
        :outdated ->
          dep.status :lockoutdated
        :ok ->
          dep
      end
    else
      dep
    end
  end

  @doc """
  Check if a dependency is ok.
  """
  def ok?(Mix.Dep[status: { :ok, _ }]), do: true
  def ok?(_), do: false

  @doc """
  Check if a dependency is available.
  """
  def available?(Mix.Dep[status: { :overriden, _ }]),    do: false
  def available?(Mix.Dep[status: { :diverged, _ }]),     do: false
  def available?(Mix.Dep[status: { :unavailable, _ }]),  do: false
  def available?(_), do: true

  @doc """
  Check if a dependency is out of date considering its
  lock status. Therefore, be sure to call `check_lock` before
  invoking this function.
  """
  def out_of_date?(Mix.Dep[status: { :lockmismatch, _ }]), do: true
  def out_of_date?(Mix.Dep[status: :lockoutdated]),        do: true
  def out_of_date?(Mix.Dep[status: :nolock]),              do: true
  def out_of_date?(Mix.Dep[status: { :elixirlock, _ }]),   do: true
  def out_of_date?(dep),                                   do: not available?(dep)

  @doc """
  Formats a dependency for printing.
  """
  def format_dep(Mix.Dep[scm: scm, app: app, status: status, opts: opts]) do
    version =
      case status do
        { :ok, vsn } when vsn != nil -> "#{vsn} "
        _ -> ""
      end

    "#{app} #{version}(#{scm.format(opts)})"
  end

  @doc """
  Returns all compile paths for the dependency.
  Expects a fetched dependency.
  """
  def compile_path(Mix.Dep[app: app, opts: opts, manager: manager]) do
    if manager == :mix do
      Mix.Project.in_project app, opts[:dest], fn _ ->
        Mix.Project.compile_path
      end
    else
      Path.join(opts[:dest], "ebin")
    end
  end

  @doc """
  Returns all load paths for the dependency.
  Expects a fetched dependency.
  """
  def load_paths(Mix.Dep[manager: :mix, app: app, opts: opts]) do
    Mix.Project.in_project(app, opts[:dest], fn _ ->
      Mix.Project.load_paths
    end) |> Enum.uniq
  end

  def load_paths(Mix.Dep[manager: :rebar, opts: opts, extra: extra]) do
    # Add root dir and all sub dirs with ebin/ directory
    sub_dirs = Enum.map(extra[:sub_dirs] || [], fn path ->
      Path.join(opts[:dest], path)
    end)

    [ opts[:dest] | sub_dirs ]
      |> Enum.map(&Path.wildcard(&1))
      |> Enum.concat
      |> Enum.map(&Path.join(&1, "ebin"))
      |> Enum.filter(&File.dir?(&1))
  end

  def load_paths(Mix.Dep[manager: manager, opts: opts]) when manager in [:make] do
    [ Path.join(opts[:dest], "ebin") ]
  end

  @doc """
  Return `true` if dependency is a mix project.
  """
  def mix?(Mix.Dep[manager: manager]) do
    manager == :mix
  end

  @doc """
  Return `true` if dependency is a rebar project.
  """
  def rebar?(Mix.Dep[manager: manager]) do
    manager == :rebar
  end

  @doc """
  Return `true` if dependency is a make project.
  """
  def make?(Mix.Dep[manager: manager]) do
    manager == :make
  end

  ## Helpers

  defp to_app_names(given) do
    Enum.map given, fn(app) ->
      if is_binary(app), do: binary_to_atom(app), else: app
    end
  end
end

defrecord Mix.Dep, [ scm: nil, app: nil, requirement: nil, status: nil, opts: nil,
                     deps: [], extra: nil, manager: nil, from: nil ] do
  @moduledoc """
  This is a record that keeps information about your project
  dependencies. It contains:

  * `scm` - a module representing the source code management tool (SCM) operations;
  * `app` - the application name as an atom;
  * `requirement` - a binary or regex with the dependency's requirement
  * `status` - the current status of the dependency, check `Mix.Deps.format_status/1` for more info;
  * `opts` - the options given by the developer
  * `deps` - dependencies of this dependency
  * `manager` - the project management, possible values: `:rebar` | `:mix` | `:make` | `nil`
  * `from` - path to the file where the dependency was defined
  * `extra` - a slot for adding extra configuration based on the scm.
              the information on this field is private to the scm and
              should not be relied on.
  """
end

defmodule Mix.Deps do
  @moduledoc %S"""
  Common functions to work with dependencies.

  Dependencies must be specified in the Mix application in the
  following format:

      { app :: atom, opts :: Keyword.t }
      { app :: atom, requirement :: String.t, opts :: Keyword.t }

  The application name must be an atom, the version requirement must
  be a string according to the specification outline in `Version`
  and opts is a keyword lists that may include options for the underlying
  SCM or options used by Mix. Each set of options is documented below.

  Inside Mix, those dependencies are converted to a `Mix.Dep` record.
  This module provides conveniences to work with those dependencies
  and the dependencies are usually in two specific states: fetched and
  unfetched.

  When a dependency is unfetched, it means Mix only parsed its specification
  and made no attempt to actually fetch the dependency or validate its
  status. When the dependency is fetched, it means Mix attempted to fetch
  and validate it, the status is set in the status field.

  ## Mix options

  * `:app` - Do not try to read the app file for this dependency
  * `:env` - The environment to run the dependency on, defaults to :prod
  * `:compile` - A command to compile the dependency, defaults to a mix,
                 rebar or make command

  ## Git options (`:git`)

  * `:git`        - The git repository URI
  * `:github`     - A shortcut for specifying git repos from github, uses `git:`
  * `:ref`        - The reference to checkout (may be a branch, a commit sha or a tag)
  * `:branch`     - The git branch to checkout
  * `:tag`        - The git tag to checkout
  * `:submodules` - When true, initialize submodules for the repo

  ## Path options (`:path`)

  * `:path` - The path for the dependency
  * `:in_umbrella` - When true, sets a path dependency pointing to "../#{app}",
                     sharing the same environment as the current application

  ## Internal options

  Those options are set internally by Mix and they can't be
  overriden from the Mixfile:

  * `:dest` - The destination path for the dependency
  * `:lock` - The lock information retrieved from mix.lock

  """

  @doc """
  Returns all children dependencies for the current project,
  as well as the defined apps in case of umbrella projects.
  The children dependencies returned by this function were
  not fetched yet.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  defdelegate children(), to: Mix.Deps.Retriever

  @doc """
  Returns fetched dependencies recursively as a `Mix.Dep` record.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def fetched do
    { deps, _ } = Mix.Deps.Converger.all(nil, fn(dep, acc) -> { dep, acc } end)
    Mix.Deps.Converger.topsort(deps)
  end

  @doc """
  Receives a list of  dependency names and returns fetched dependency
  records. Logs a message if the dependency could not be found.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def fetched_by_name(given, all_deps // fetched) do
    # Ensure all apps are atoms
    apps = to_app_names(given)

    # We need to keep the order of all, which properly orders deps
    deps = Enum.filter all_deps, fn(dep) -> dep.app in apps end

    # Now we validate the given atoms
    index = Mix.Dep.__record__(:index, :app)
    Enum.each apps, fn(app) ->
      unless List.keyfind(deps, app, index) do
        Mix.shell.info "unknown dependency #{app} for env #{Mix.env}"
      end
    end

    deps
  end

  @doc """
  Maps and reduces over all unfetched dependencies, one by one.

  This is useful in case you want to retrieve the dependency
  tree for a project but process and change them along the way.
  For example, `mix deps.get` uses it to get all dependencies
  by first fetching the parent and then updating the tree as it goes.

  The callback expects the current dependency and the accumulator
  as arguments. The accumulator is returned as result.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def unfetched(acc, callback) do
    { _deps, acc } = Mix.Deps.Converger.all(acc, callback)
    acc
  end

  @doc """
  Receives a list of dependency names and maps and reduces over
  them.

  ## Exceptions

  This function raises an exception if any of the dependencies
  provided in the project are in the wrong format.
  """
  def unfetched_by_name(given, acc, callback) do
    names = to_app_names(given)

    unfetched(acc, fn(dep, acc) ->
      if dep.app in names do
        callback.(dep, acc)
      else
        { dep, acc }
      end
    end)
  end

  @doc """
  Receives a list of dependencies and returns the given list
  with their depending dependencies, recursively. It is expected
  the given list of dependencies to contain only fetched dependencies
  (since unfetched dependencies did not have their dependencies
  retrieved yet).
  """
  def with_depending(deps, all_deps // fetched) do
    (deps ++ do_with_depending(deps, all_deps))
    |> Enum.uniq(&(&1.app))
  end

  defp do_with_depending([], _all_deps) do
    []
  end

  defp do_with_depending(deps, all_deps) do
    dep_names = Enum.map(deps, fn dep -> dep.app end)

    parents = Enum.filter all_deps, fn dep ->
      Enum.any?(dep.deps, fn child_dep -> child_dep.app in dep_names end)
    end

    do_with_depending(parents, all_deps) ++ parents
  end

  @doc """
  Runs the given `fun` inside the given dependency project by
  changing the current working directory and loading the given
  project onto the project stack.

  In case the project is a rebar dependency, a `Mix.Rebar` project
  will be pushed into the stack to simulate the rebar configuration.

  It is expected a fetched dependency as argument.
  """
  def in_dependency(dep, post_config // [], fun)

  def in_dependency(Mix.Dep[manager: :rebar, opts: opts], post_config, fun) do
    # Use post_config for rebar deps
    Mix.Project.post_config(post_config)
    Mix.Project.push(Mix.Rebar)
    try do
      File.cd!(opts[:dest], fn -> fun.(Mix.Rebar) end)
    after
      Mix.Project.pop
    end
  end

  def in_dependency(Mix.Dep[app: app, opts: opts], post_config, fun) do
    env     = opts[:env] || :prod
    old_env = Mix.env

    try do
      Mix.env(env)
      Mix.Project.in_project(app, opts[:dest], post_config, fun)
    after
      Mix.env(old_env)
    end
  end

  @doc """
  Formats the status of a dependency.
  """
  def format_status(Mix.Dep[status: { :ok, _vsn }]),
    do: "ok"

  def format_status(Mix.Dep[status: { :noappfile, path }]),
    do: "could not find an app file at #{Path.relative_to_cwd(path)}, " <>
        "this may happen when you specified the wrong application name in your deps " <>
        "or if the dependency failed to compile (which can be amended with `mix deps.compile`)"

  def format_status(Mix.Dep[status: { :invalidapp, path }]),
    do: "the app file at #{Path.relative_to_cwd(path)} is invalid"

  def format_status(Mix.Dep[status: { :invalidvsn, vsn }]),
    do: "the app file contains an invalid version: #{inspect vsn}"

  def format_status(Mix.Dep[status: { :nomatchvsn, vsn }, requirement: req]),
    do: "the dependency does not match the requirement #{inspect req}, got #{inspect vsn}"

  def format_status(Mix.Dep[status: { :lockmismatch, _ }]),
    do: "lock mismatch: the dependency is out of date"

  def format_status(Mix.Dep[status: :lockoutdated]),
    do: "lock outdated: the lock is outdated compared to the options in your mixfile"

  def format_status(Mix.Dep[status: :nolock]),
    do: "the dependency is not locked"

  def format_status(Mix.Dep[app: app, status: { :diverged, other }] = dep) do
    "different specs were given for the #{inspect app} app:\n" <>
    "#{dep_status(dep)}#{dep_status(other)}" <>
    "\n  Ensure they match or specify one in your #{inspect Mix.Project.get} deps and set `override: true`"
  end

  def format_status(Mix.Dep[app: app, status: { :overriden, other }] = dep) do
    "the dependency #{app} in #{Path.relative_to_cwd(dep.from)} is overriding a child dependency:\n" <>
    "#{dep_status(dep)}#{dep_status(other)}" <>
    "\n  Specify one in your #{inspect Mix.Project.get} deps and set `override: true`"
  end

  def format_status(Mix.Dep[status: { :unavailable, _ }]),
    do: "the dependency is not available, run `mix deps.get`"

  def format_status(Mix.Dep[status: { :elixirlock, _ }]),
    do: "the dependency is built with an out-of-date elixir version, run `mix deps.get`"

  defp dep_status(Mix.Dep[app: app, requirement: req, opts: opts, from: from]) do
    info = { app, req, Dict.drop(opts, [:dest, :lock, :env]) }
    "\n  > In #{Path.relative_to_cwd(from)}:\n    #{inspect info}\n"
  end

  @doc """
  Checks the lock for the given dependency and update its status accordingly.
  """
  def check_lock(Mix.Dep[scm: scm, app: app, opts: opts] = dep, lock) do
    if available?(dep) do
      rev  = lock[app]
      opts = Keyword.put(opts, :lock, rev)

      case scm.lock_status(opts) do
        :mismatch ->
          dep.status(if rev, do: { :lockmismatch, rev }, else: :nolock)
        :outdated ->
          dep.status :lockoutdated
        :ok ->
          dep
      end
    else
      dep
    end
  end

  @doc """
  Check if a dependency is ok.
  """
  def ok?(Mix.Dep[status: { :ok, _ }]), do: true
  def ok?(_), do: false

  @doc """
  Check if a dependency is available.
  """
  def available?(Mix.Dep[status: { :overriden, _ }]),    do: false
  def available?(Mix.Dep[status: { :diverged, _ }]),     do: false
  def available?(Mix.Dep[status: { :unavailable, _ }]),  do: false
  def available?(_), do: true

  @doc """
  Check if a dependency is out of date considering its
  lock status. Therefore, be sure to call `check_lock` before
  invoking this function.
  """
  def out_of_date?(Mix.Dep[status: { :lockmismatch, _ }]), do: true
  def out_of_date?(Mix.Dep[status: :lockoutdated]),        do: true
  def out_of_date?(Mix.Dep[status: :nolock]),              do: true
  def out_of_date?(Mix.Dep[status: { :elixirlock, _ }]),   do: true
  def out_of_date?(dep),                                   do: not available?(dep)

  @doc """
  Formats a dependency for printing.
  """
  def format_dep(Mix.Dep[scm: scm, app: app, status: status, opts: opts]) do
    version =
      case status do
        { :ok, vsn } when vsn != nil -> "#{vsn} "
        _ -> ""
      end

    "#{app} #{version}(#{scm.format(opts)})"
  end

  @doc """
  Returns all compile paths for the dependency.
  Expects a fetched dependency.
  """
  def compile_path(Mix.Dep[app: app, opts: opts, manager: manager]) do
    if manager == :mix do
      Mix.Project.in_project app, opts[:dest], fn _ ->
        Mix.Project.compile_path
      end
    else
      Path.join(opts[:dest], "ebin")
    end
  end

  @doc """
  Returns all load paths for the dependency.
  Expects a fetched dependency.
  """
  def load_paths(Mix.Dep[manager: :mix, app: app, opts: opts]) do
    Mix.Project.in_project(app, opts[:dest], fn _ ->
      Mix.Project.load_paths
    end) |> Enum.uniq
  end

  def load_paths(Mix.Dep[manager: :rebar, opts: opts, extra: extra]) do
    # Add root dir and all sub dirs with ebin/ directory
    sub_dirs = Enum.map(extra[:sub_dirs] || [], fn path ->
      Path.join(opts[:dest], path)
    end)

    [ opts[:dest] | sub_dirs ]
      |> Enum.map(&Path.wildcard(&1))
      |> Enum.concat
      |> Enum.map(&Path.join(&1, "ebin"))
      |> Enum.filter(&File.dir?(&1))
  end

  def load_paths(Mix.Dep[manager: manager, opts: opts]) when manager in [:make] do
    [ Path.join(opts[:dest], "ebin") ]
  end

  @doc """
  Return `true` if dependency is a mix project.
  """
  def mix?(Mix.Dep[manager: manager]) do
    manager == :mix
  end

  @doc """
  Return `true` if dependency is a rebar project.
  """
  def rebar?(Mix.Dep[manager: manager]) do
    manager == :rebar
  end

  @doc """
  Return `true` if dependency is a make project.
  """
  def make?(Mix.Dep[manager: manager]) do
    manager == :make
  end

  ## Helpers

  defp to_app_names(given) do
    Enum.map given, fn(app) ->
      if is_binary(app), do: binary_to_atom(app), else: app
    end
  end
end


Correct version

Correct version

defmodule Credo.Backports.Enum do
  @moduledoc """
  This module provides Enum functions which are not supported in all Elixir
  versions supported by Credo.
  """

  @doc """
  Splits the enumerable in two lists according to the given function fun.

      iex> Credo.Backports.Enum.split_with([1, 2, 3, 4, 5], &(Integer.mod(&1, 2) == 0))
      {[2, 4], [1, 3, 5]}

      iex> Credo.Backports.Enum.split_with(["hello", "same", "sample"], &(String.starts_with?(&1, "he")))
      {["hello"], ["same", "sample"]}

  """
  def split_with(list, fun)

  if Version.match?(System.version, "< 1.6.0") do
    def split_with(list, fun), do: Enum.partition(list, fun)
  else
    def split_with(list, fun), do: Enum.split_with(list, fun)
  end
end

defmodule Credo.Backports.Enum do
  @moduledoc """
  This module provides Enum functions which are not supported in all Elixir
  versions supported by Credo.
  """

  @doc """
  Splits the enumerable in two lists according to the given function fun.

      iex> Credo.Backports.Enum.split_with([1, 2, 3, 4, 5], &(Integer.mod(&1, 2) == 0))
      {[2, 4], [1, 3, 5]}

      iex> Credo.Backports.Enum.split_with(["hello", "same", "sample"], &(String.starts_with?(&1, "he")))
      {["hello"], ["same", "sample"]}

  """
  def split_with(list, fun)

  if Version.match?(System.version, "< 1.3.0") do
    def split_with(list, fun), do: Enum.partition(list, fun)
  else
    def split_with(list, fun), do: Enum.split_with(list, fun)
  end
end


Allow atime, mtime, ctime to have integer() types (#8839)

require Record

defmodule File.Stat do
  @moduledoc """
  A struct that holds file information.

  In Erlang, this struct is represented by a `:file_info` record.
  Therefore this module also provides functions for converting
  between the Erlang record and the Elixir struct.

  Its fields are:

    * `size` - size of file in bytes.

    * `type` - `:device | :directory | :regular | :other | :symlink`; the type of the
      file.

    * `access` - `:read | :write | :read_write | :none`; the current system
      access to the file.

    * `atime` - the last time the file was read.

    * `mtime` - the last time the file was written.

    * `ctime` - the interpretation of this time field depends on the operating
      system. On Unix, it is the last time the file or the inode was changed.
      In Windows, it is the time of creation.

    * `mode` - the file permissions.

    * `links` - the number of links to this file. This is always 1 for file
      systems which have no concept of links.

    * `major_device` - identifies the file system where the file is located.
      In Windows, the number indicates a drive as follows: 0 means A:, 1 means
      B:, and so on.

    * `minor_device` - only valid for character devices on Unix. In all other
      cases, this field is zero.

    * `inode` - gives the inode number. On non-Unix file systems, this field
      will be zero.

    * `uid` - indicates the owner of the file. Will be zero for non-Unix file
      systems.

    * `gid` - indicates the group that owns the file. Will be zero for
      non-Unix file systems.

  The time type returned in `atime`, `mtime`, and `ctime` is dependent on the
  time type set in options. `{:time, type}` where type can be `:local`,
  `:universal`, or `:posix`. Default is `:universal`.
  """

  record = Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
  keys = :lists.map(&elem(&1, 0), record)
  vals = :lists.map(&{&1, [], nil}, keys)
  pairs = :lists.zip(keys, vals)

  defstruct keys

  @type t :: %__MODULE__{
          size: non_neg_integer(),
          type: :device | :directory | :regular | :other | :symlink,
          access: :read | :write | :read_write | :none,
          atime: :calendar.datetime() | integer(),
          mtime: :calendar.datetime() | integer(),
          ctime: :calendar.datetime() | integer(),
          mode: non_neg_integer(),
          links: non_neg_integer(),
          major_device: non_neg_integer(),
          minor_device: non_neg_integer(),
          inode: non_neg_integer(),
          uid: non_neg_integer(),
          gid: non_neg_integer()
        }

  @doc """
  Converts a `File.Stat` struct to a `:file_info` record.
  """
  @spec to_record(t()) :: :file.file_info()
  def to_record(%File.Stat{unquote_splicing(pairs)}) do
    {:file_info, unquote_splicing(vals)}
  end

  @doc """
  Converts a `:file_info` record into a `File.Stat`.
  """
  @spec from_record(:file.file_info()) :: t()
  def from_record(file_info)

  def from_record({:file_info, unquote_splicing(vals)}) do
    %File.Stat{unquote_splicing(pairs)}
  end
end

require Record

defmodule File.Stat do
  @moduledoc """
  A struct that holds file information.

  In Erlang, this struct is represented by a `:file_info` record.
  Therefore this module also provides functions for converting
  between the Erlang record and the Elixir struct.

  Its fields are:

    * `size` - size of file in bytes.

    * `type` - `:device | :directory | :regular | :other | :symlink`; the type of the
      file.

    * `access` - `:read | :write | :read_write | :none`; the current system
      access to the file.

    * `atime` - the last time the file was read.

    * `mtime` - the last time the file was written.

    * `ctime` - the interpretation of this time field depends on the operating
      system. On Unix, it is the last time the file or the inode was changed.
      In Windows, it is the time of creation.

    * `mode` - the file permissions.

    * `links` - the number of links to this file. This is always 1 for file
      systems which have no concept of links.

    * `major_device` - identifies the file system where the file is located.
      In Windows, the number indicates a drive as follows: 0 means A:, 1 means
      B:, and so on.

    * `minor_device` - only valid for character devices on Unix. In all other
      cases, this field is zero.

    * `inode` - gives the inode number. On non-Unix file systems, this field
      will be zero.

    * `uid` - indicates the owner of the file. Will be zero for non-Unix file
      systems.

    * `gid` - indicates the group that owns the file. Will be zero for
      non-Unix file systems.

  The time type returned in `atime`, `mtime`, and `ctime` is dependent on the
  time type set in options. `{:time, type}` where type can be `:local`,
  `:universal`, or `:posix`. Default is `:universal`.
  """

  record = Record.extract(:file_info, from_lib: "kernel/include/file.hrl")
  keys = :lists.map(&elem(&1, 0), record)
  vals = :lists.map(&{&1, [], nil}, keys)
  pairs = :lists.zip(keys, vals)

  defstruct keys

  @type t :: %__MODULE__{
          size: non_neg_integer(),
          type: :device | :directory | :regular | :other | :symlink,
          access: :read | :write | :read_write | :none,
          atime: :calendar.datetime(),
          mtime: :calendar.datetime(),
          ctime: :calendar.datetime(),
          mode: non_neg_integer(),
          links: non_neg_integer(),
          major_device: non_neg_integer(),
          minor_device: non_neg_integer(),
          inode: non_neg_integer(),
          uid: non_neg_integer(),
          gid: non_neg_integer()
        }

  @doc """
  Converts a `File.Stat` struct to a `:file_info` record.
  """
  @spec to_record(t()) :: :file.file_info()
  def to_record(%File.Stat{unquote_splicing(pairs)}) do
    {:file_info, unquote_splicing(vals)}
  end

  @doc """
  Converts a `:file_info` record into a `File.Stat`.
  """
  @spec from_record(:file.file_info()) :: t()
  def from_record(file_info)

  def from_record({:file_info, unquote_splicing(vals)}) do
    %File.Stat{unquote_splicing(pairs)}
  end
end


Run code formatter on lib/elixir/lib/string_io.ex (#6760)

defmodule StringIO do
  @moduledoc """
  Controls an IO device process that wraps a string.

  A `StringIO` IO device can be passed as a "device" to
  most of the functions in the `IO` module.

  ## Examples

      iex> {:ok, pid} = StringIO.open("foo")
      iex> IO.read(pid, 2)
      "fo"

  """

  use GenServer

  @doc """
  Creates an IO device.

  `string` will be the initial input of the newly created
  device.

  If the `:capture_prompt` option is set to `true`,
  prompts (specified as arguments to `IO.get*` functions)
  are captured.

  ## Examples

      iex> {:ok, pid} = StringIO.open("foo")
      iex> IO.gets(pid, ">")
      "foo"
      iex> StringIO.contents(pid)
      {"", ""}

      iex> {:ok, pid} = StringIO.open("foo", capture_prompt: true)
      iex> IO.gets(pid, ">")
      "foo"
      iex> StringIO.contents(pid)
      {"", ">"}

  """
  @spec open(binary, keyword) :: {:ok, pid}
  def open(string, options \\ []) when is_binary(string) do
    GenServer.start_link(__MODULE__, {string, options}, [])
  end

  @doc """
  Returns the current input/output buffers for the given IO
  device.

  ## Examples

      iex> {:ok, pid} = StringIO.open("in")
      iex> IO.write(pid, "out")
      iex> StringIO.contents(pid)
      {"in", "out"}

  """
  @spec contents(pid) :: {binary, binary}
  def contents(pid) when is_pid(pid) do
    GenServer.call(pid, :contents)
  end

  @doc """
  Flushes the output buffer and returns its current contents.

  ## Examples

      iex> {:ok, pid} = StringIO.open("in")
      iex> IO.write(pid, "out")
      iex> StringIO.flush(pid)
      "out"
      iex> StringIO.contents(pid)
      {"in", ""}

  """
  @spec flush(pid) :: binary
  def flush(pid) when is_pid(pid) do
    GenServer.call(pid, :flush)
  end

  @doc """
  Stops the IO device and returns the remaining input/output
  buffers.

  ## Examples

      iex> {:ok, pid} = StringIO.open("in")
      iex> IO.write(pid, "out")
      iex> StringIO.close(pid)
      {:ok, {"in", "out"}}

  """
  @spec close(pid) :: {:ok, {binary, binary}}
  def close(pid) when is_pid(pid) do
    GenServer.call(pid, :close)
  end

  ## callbacks

  def init({string, options}) do
    capture_prompt = options[:capture_prompt] || false
    {:ok, %{input: string, output: "", capture_prompt: capture_prompt}}
  end

  def handle_info({:io_request, from, reply_as, req}, state) do
    state = io_request(from, reply_as, req, state)
    {:noreply, state}
  end

  def handle_info(message, state) do
    super(message, state)
  end

  def handle_call(:contents, _from, %{input: input, output: output} = state) do
    {:reply, {input, output}, state}
  end

  def handle_call(:flush, _from, %{output: output} = state) do
    {:reply, output, %{state | output: ""}}
  end

  def handle_call(:close, _from, %{input: input, output: output} = state) do
    {:stop, :normal, {:ok, {input, output}}, state}
  end

  def handle_call(request, from, state) do
    super(request, from, state)
  end

  defp io_request(from, reply_as, req, state) do
    {reply, state} = io_request(req, state)
    io_reply(from, reply_as, to_reply(reply))
    state
  end

  defp io_request({:put_chars, chars} = req, state) do
    put_chars(:latin1, chars, req, state)
  end

  defp io_request({:put_chars, mod, fun, args} = req, state) do
    put_chars(:latin1, apply(mod, fun, args), req, state)
  end

  defp io_request({:put_chars, encoding, chars} = req, state) do
    put_chars(encoding, chars, req, state)
  end

  defp io_request({:put_chars, encoding, mod, fun, args} = req, state) do
    put_chars(encoding, apply(mod, fun, args), req, state)
  end

  defp io_request({:get_chars, prompt, count}, state) when count >= 0 do
    io_request({:get_chars, :latin1, prompt, count}, state)
  end

  defp io_request({:get_chars, encoding, prompt, count}, state) when count >= 0 do
    get_chars(encoding, prompt, count, state)
  end

  defp io_request({:get_line, prompt}, state) do
    io_request({:get_line, :latin1, prompt}, state)
  end

  defp io_request({:get_line, encoding, prompt}, state) do
    get_line(encoding, prompt, state)
  end

  defp io_request({:get_until, prompt, mod, fun, args}, state) do
    io_request({:get_until, :latin1, prompt, mod, fun, args}, state)
  end

  defp io_request({:get_until, encoding, prompt, mod, fun, args}, state) do
    get_until(encoding, prompt, mod, fun, args, state)
  end

  defp io_request({:get_password, encoding}, state) do
    get_line(encoding, "", state)
  end

  defp io_request({:setopts, _opts}, state) do
    {{:error, :enotsup}, state}
  end

  defp io_request(:getopts, state) do
    {{:ok, [binary: true, encoding: :unicode]}, state}
  end

  defp io_request({:get_geometry, :columns}, state) do
    {{:error, :enotsup}, state}
  end

  defp io_request({:get_geometry, :rows}, state) do
    {{:error, :enotsup}, state}
  end

  defp io_request({:requests, reqs}, state) do
    io_requests(reqs, {:ok, state})
  end

  defp io_request(_, state) do
    {{:error, :request}, state}
  end

  ## put_chars

  defp put_chars(encoding, chars, req, %{output: output} = state) do
    case :unicode.characters_to_binary(chars, encoding, :unicode) do
      string when is_binary(string) ->
        {:ok, %{state | output: output <> string}}

      {_, _, _} ->
        {{:error, req}, state}
    end
  end

  ## get_chars

  defp get_chars(encoding, prompt, count, %{input: input} = state) do
    case get_chars(input, encoding, count) do
      {:error, _} = error ->
        {error, state}

      {result, input} ->
        {result, state_after_read(state, input, prompt, 1)}
    end
  end

  defp get_chars("", _encoding, _count) do
    {:eof, ""}
  end

  defp get_chars(input, :latin1, count) when byte_size(input) < count do
    {input, ""}
  end

  defp get_chars(input, :latin1, count) do
    <<chars::binary-size(count), rest::binary>> = input
    {chars, rest}
  end

  defp get_chars(input, encoding, count) do
    try do
      case :file_io_server.count_and_find(input, count, encoding) do
        {buf_count, split_pos} when buf_count < count or split_pos == :none ->
          {input, ""}

        {_buf_count, split_pos} ->
          <<chars::binary-size(split_pos), rest::binary>> = input
          {chars, rest}
      end
    catch
      :exit, :invalid_unicode ->
        {:error, :invalid_unicode}
    end
  end

  ## get_line

  defp get_line(encoding, prompt, %{input: input} = state) do
    case bytes_until_eol(input, encoding, 0) do
      {:split, 0} ->
        {:eof, state_after_read(state, "", prompt, 1)}

      {:split, count} ->
        {result, remainder} = :erlang.split_binary(input, count)
        {result, state_after_read(state, remainder, prompt, 1)}

      {:replace_split, count} ->
        {result, remainder} = :erlang.split_binary(input, count)
        result = binary_part(result, 0, byte_size(result) - 2) <> "\n"
        {result, state_after_read(state, remainder, prompt, 1)}

      :error ->
        {{:error, :collect_line}, state}
    end
  end

  ## get_until

  defp get_until(encoding, prompt, mod, fun, args, %{input: input} = state) do
    case get_until(input, encoding, mod, fun, args, [], 0) do
      {result, input, count} ->
        input =
          case input do
            :eof -> ""
            _ -> list_to_binary(input, encoding)
          end

        {get_until_result(result, encoding), state_after_read(state, input, prompt, count)}

      :error ->
        {:error, state}
    end
  end

  defp get_until("", encoding, mod, fun, args, continuation, count) do
    case apply(mod, fun, [continuation, :eof | args]) do
      {:done, result, rest} ->
        {result, rest, count + 1}

      {:more, next_continuation} ->
        get_until("", encoding, mod, fun, args, next_continuation, count + 1)
    end
  end

  defp get_until(chars, encoding, mod, fun, args, continuation, count) do
    case bytes_until_eol(chars, encoding, 0) do
      {kind, size} when kind in [:split, :replace_split] ->
        {line, rest} = :erlang.split_binary(chars, size)

        case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do
          {:done, result, :eof} ->
            {result, rest, count + 1}

          {:done, result, extra} ->
            {result, extra ++ binary_to_list(rest, encoding), count + 1}

          {:more, next_continuation} ->
            get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
        end

      :error ->
        :error
    end
  end

  defp binary_to_list(data, _) when is_list(data), do: data
  defp binary_to_list(data, :unicode) when is_binary(data), do: String.to_charlist(data)
  defp binary_to_list(data, :latin1) when is_binary(data), do: :erlang.binary_to_list(data)

  defp list_to_binary(data, _) when is_binary(data), do: data
  defp list_to_binary(data, :unicode) when is_list(data), do: List.to_string(data)
  defp list_to_binary(data, :latin1) when is_list(data), do: :erlang.list_to_binary(data)

  # From http://erlang.org/doc/apps/stdlib/io_protocol.html: result can be any
  # Erlang term, but if it is a list(), the I/O server can convert it to a binary().
  defp get_until_result(data, encoding) when is_list(data), do: list_to_binary(data, encoding)
  defp get_until_result(data, _), do: data

  ## io_requests

  defp io_requests([req | rest], {:ok, state}) do
    io_requests(rest, io_request(req, state))
  end

  defp io_requests(_, result) do
    result
  end

  ## helpers

  defp state_after_read(%{capture_prompt: false} = state, remainder, _prompt, _count) do
    %{state | input: remainder}
  end

  defp state_after_read(%{capture_prompt: true, output: output} = state, remainder, prompt, count) do
    output = <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>
    %{state | input: remainder, output: output}
  end

  defp bytes_until_eol("", _, count), do: {:split, count}
  defp bytes_until_eol(<<"\r\n"::binary, _::binary>>, _, count), do: {:replace_split, count + 2}
  defp bytes_until_eol(<<"\n"::binary, _::binary>>, _, count), do: {:split, count + 1}

  defp bytes_until_eol(<<head::utf8, tail::binary>>, :unicode, count) do
    bytes_until_eol(tail, :unicode, count + byte_size(<<head::utf8>>))
  end

  defp bytes_until_eol(<<_, tail::binary>>, :latin1, count) do
    bytes_until_eol(tail, :latin1, count + 1)
  end

  defp bytes_until_eol(<<_::binary>>, _, _), do: :error

  defp io_reply(from, reply_as, reply) do
    send(from, {:io_reply, reply_as, reply})
  end

  defp to_reply(list) when is_list(list), do: IO.chardata_to_string(list)
  defp to_reply(other), do: other
end

defmodule StringIO do
  @moduledoc """
  Controls an IO device process that wraps a string.

  A `StringIO` IO device can be passed as a "device" to
  most of the functions in the `IO` module.

  ## Examples

      iex> {:ok, pid} = StringIO.open("foo")
      iex> IO.read(pid, 2)
      "fo"

  """

  use GenServer

  @doc """
  Creates an IO device.

  `string` will be the initial input of the newly created
  device.

  If the `:capture_prompt` option is set to `true`,
  prompts (specified as arguments to `IO.get*` functions)
  are captured.

  ## Examples

      iex> {:ok, pid} = StringIO.open("foo")
      iex> IO.gets(pid, ">")
      "foo"
      iex> StringIO.contents(pid)
      {"", ""}

      iex> {:ok, pid} = StringIO.open("foo", capture_prompt: true)
      iex> IO.gets(pid, ">")
      "foo"
      iex> StringIO.contents(pid)
      {"", ">"}

  """
  @spec open(binary, keyword) :: {:ok, pid}
  def open(string, options \\ []) when is_binary(string) do
    GenServer.start_link(__MODULE__, {string, options}, [])
  end

  @doc """
  Returns the current input/output buffers for the given IO
  device.

  ## Examples

      iex> {:ok, pid} = StringIO.open("in")
      iex> IO.write(pid, "out")
      iex> StringIO.contents(pid)
      {"in", "out"}

  """
  @spec contents(pid) :: {binary, binary}
  def contents(pid) when is_pid(pid) do
    GenServer.call(pid, :contents)
  end

  @doc """
  Flushes the output buffer and returns its current contents.

  ## Examples

      iex> {:ok, pid} = StringIO.open("in")
      iex> IO.write(pid, "out")
      iex> StringIO.flush(pid)
      "out"
      iex> StringIO.contents(pid)
      {"in", ""}

  """
  @spec flush(pid) :: binary
  def flush(pid) when is_pid(pid) do
    GenServer.call(pid, :flush)
  end

  @doc """
  Stops the IO device and returns the remaining input/output
  buffers.

  ## Examples

      iex> {:ok, pid} = StringIO.open("in")
      iex> IO.write(pid, "out")
      iex> StringIO.close(pid)
      {:ok, {"in", "out"}}

  """
  @spec close(pid) :: {:ok, {binary, binary}}
  def close(pid) when is_pid(pid) do
    GenServer.call(pid, :close)
  end

  ## callbacks

  def init({string, options}) do
    capture_prompt = options[:capture_prompt] || false
    {:ok, %{input: string, output: "", capture_prompt: capture_prompt}}
  end

  def handle_info({:io_request, from, reply_as, req}, state) do
    state = io_request(from, reply_as, req, state)
    {:noreply, state}
  end

  def handle_info(message, state) do
    super(message, state)
  end

  def handle_call(:contents, _from, %{input: input, output: output} = state) do
    {:reply, {input, output}, state}
  end

  def handle_call(:flush, _from, %{output: output} = state) do
    {:reply, output, %{state | output: ""}}
  end

  def handle_call(:close, _from, %{input: input, output: output} = state) do
    {:stop, :normal, {:ok, {input, output}}, state}
  end

  def handle_call(request, from, state) do
    super(request, from, state)
  end

  defp io_request(from, reply_as, req, state) do
    {reply, state} = io_request(req, state)
    io_reply(from, reply_as, to_reply(reply))
    state
  end

  defp io_request({:put_chars, chars} = req, state) do
    put_chars(:latin1, chars, req, state)
  end

  defp io_request({:put_chars, mod, fun, args} = req, state) do
    put_chars(:latin1, apply(mod, fun, args), req, state)
  end

  defp io_request({:put_chars, encoding, chars} = req, state) do
    put_chars(encoding, chars, req, state)
  end

  defp io_request({:put_chars, encoding, mod, fun, args} = req, state) do
    put_chars(encoding, apply(mod, fun, args), req, state)
  end

  defp io_request({:get_chars, prompt, count}, state) when count >= 0 do
    io_request({:get_chars, :latin1, prompt, count}, state)
  end

  defp io_request({:get_chars, encoding, prompt, count}, state) when count >= 0 do
    get_chars(encoding, prompt, count, state)
  end

  defp io_request({:get_line, prompt}, state) do
    io_request({:get_line, :latin1, prompt}, state)
  end

  defp io_request({:get_line, encoding, prompt}, state) do
    get_line(encoding, prompt, state)
  end

  defp io_request({:get_until, prompt, mod, fun, args}, state) do
    io_request({:get_until, :latin1, prompt, mod, fun, args}, state)
  end

  defp io_request({:get_until, encoding, prompt, mod, fun, args}, state) do
    get_until(encoding, prompt, mod, fun, args, state)
  end

  defp io_request({:get_password, encoding}, state) do
    get_line(encoding, "", state)
  end

  defp io_request({:setopts, _opts}, state) do
    {{:error, :enotsup}, state}
  end

  defp io_request(:getopts, state) do
    {{:ok, [binary: true, encoding: :unicode]}, state}
  end

  defp io_request({:get_geometry, :columns}, state) do
    {{:error, :enotsup}, state}
  end

  defp io_request({:get_geometry, :rows}, state) do
    {{:error, :enotsup}, state}
  end

  defp io_request({:requests, reqs}, state) do
    io_requests(reqs, {:ok, state})
  end

  defp io_request(_, state) do
    {{:error, :request}, state}
  end

  ## put_chars

  defp put_chars(encoding, chars, req, %{output: output} = state) do
    case :unicode.characters_to_binary(chars, encoding, :unicode) do
      string when is_binary(string) ->
        {:ok, %{state | output: output <> string}}
      {_, _, _} ->
        {{:error, req}, state}
    end
  end

  ## get_chars

  defp get_chars(encoding, prompt, count, %{input: input} = state) do
    case get_chars(input, encoding, count) do
      {:error, _} = error ->
        {error, state}
      {result, input} ->
        {result, state_after_read(state, input, prompt, 1)}
    end
  end

  defp get_chars("", _encoding, _count) do
    {:eof, ""}
  end

  defp get_chars(input, :latin1, count) when byte_size(input) < count do
    {input, ""}
  end

  defp get_chars(input, :latin1, count) do
    <<chars::binary-size(count), rest::binary>> = input
    {chars, rest}
  end

  defp get_chars(input, encoding, count) do
    try do
      case :file_io_server.count_and_find(input, count, encoding) do
        {buf_count, split_pos} when buf_count < count or split_pos == :none ->
          {input, ""}
        {_buf_count, split_pos} ->
          <<chars::binary-size(split_pos), rest::binary>> = input
          {chars, rest}
      end
    catch
      :exit, :invalid_unicode ->
        {:error, :invalid_unicode}
    end
  end

  ## get_line

  defp get_line(encoding, prompt, %{input: input} = state) do
    case bytes_until_eol(input, encoding, 0) do
      {:split, 0} ->
        {:eof, state_after_read(state, "", prompt, 1)}
      {:split, count} ->
        {result, remainder} = :erlang.split_binary(input, count)

        {result, state_after_read(state, remainder, prompt, 1)}
      {:replace_split, count} ->
        {result, remainder} = :erlang.split_binary(input, count)

        {binary_part(result, 0, byte_size(result) - 2) <> "\n", state_after_read(state, remainder, prompt, 1)}
      :error
        -> {{:error, :collect_line}, state}
    end
  end

  ## get_until

  defp get_until(encoding, prompt, mod, fun, args, %{input: input} = state) do
    case get_until(input, encoding, mod, fun, args, [], 0) do
      {result, input, count} ->
        input =
          case input do
            :eof -> ""
            _ -> list_to_binary(input, encoding)
          end

        {get_until_result(result, encoding), state_after_read(state, input, prompt, count)}

      :error ->
        {:error, state}
    end
  end

  defp get_until("", encoding, mod, fun, args, continuation, count) do
    case apply(mod, fun, [continuation, :eof | args]) do
      {:done, result, rest} ->
        {result, rest, count + 1}
      {:more, next_continuation} ->
        get_until("", encoding, mod, fun, args, next_continuation, count + 1)
    end
  end

  defp get_until(chars, encoding, mod, fun, args, continuation, count) do
    case bytes_until_eol(chars, encoding, 0) do
      {kind, size} when kind in [:split, :replace_split] ->
        {line, rest} = :erlang.split_binary(chars, size)

        case apply(mod, fun, [continuation, binary_to_list(line, encoding) | args]) do
          {:done, result, :eof} ->
            {result, rest, count + 1}
          {:done, result, extra} ->
            {result, extra ++ binary_to_list(rest, encoding), count + 1}
          {:more, next_continuation} ->
            get_until(rest, encoding, mod, fun, args, next_continuation, count + 1)
        end

      :error ->
        :error
    end
  end

  defp binary_to_list(data, _) when is_list(data), do: data
  defp binary_to_list(data, :unicode) when is_binary(data), do: String.to_charlist(data)
  defp binary_to_list(data, :latin1) when is_binary(data), do: :erlang.binary_to_list(data)

  defp list_to_binary(data, _) when is_binary(data), do: data
  defp list_to_binary(data, :unicode) when is_list(data), do: List.to_string(data)
  defp list_to_binary(data, :latin1) when is_list(data), do: :erlang.list_to_binary(data)

  # From http://erlang.org/doc/apps/stdlib/io_protocol.html: result can be any
  # Erlang term, but if it is a list(), the I/O server can convert it to a binary().
  defp get_until_result(data, encoding) when is_list(data), do: list_to_binary(data, encoding)
  defp get_until_result(data, _), do: data

  ## io_requests

  defp io_requests([req | rest], {:ok, state}) do
    io_requests(rest, io_request(req, state))
  end

  defp io_requests(_, result) do
    result
  end

  ## helpers

  defp state_after_read(%{capture_prompt: false} = state, remainder, _prompt, _count) do
    %{state | input: remainder}
  end

  defp state_after_read(%{capture_prompt: true, output: output} = state, remainder, prompt, count) do
    %{state | input: remainder, output: <<output::binary, :binary.copy(IO.chardata_to_string(prompt), count)::binary>>}
  end

  defp bytes_until_eol("", _, count), do: {:split, count}
  defp bytes_until_eol(<<"\r\n"::binary, _::binary>>, _, count), do: {:replace_split, count + 2}
  defp bytes_until_eol(<<"\n"::binary, _::binary>>, _, count), do: {:split, count + 1}

  defp bytes_until_eol(<<head::utf8, tail::binary>>, :unicode, count) do
    bytes_until_eol(tail, :unicode, count + byte_size(<<head::utf8>>))
  end

  defp bytes_until_eol(<<_, tail::binary>>, :latin1, count) do
    bytes_until_eol(tail, :latin1, count + 1)
  end

  defp bytes_until_eol(<<_::binary>>, _, _), do: :error

  defp io_reply(from, reply_as, reply) do
    send from, {:io_reply, reply_as, reply}
  end

  defp to_reply(list) when is_list(list), do: IO.chardata_to_string(list)
  defp to_reply(other), do: other
end