lib/bylaw/credo/check/ecto/use_my_app_schema.ex

defmodule Bylaw.Credo.Check.Ecto.UseMyAppSchema do
  @moduledoc """
  `use Ecto.Schema` should not be used directly. Use your app schema module instead.

  ## Examples

  For example:

  ```elixir
  defmodule MyApp.Schema do
    defmacro __using__(_opts) do
      quote do
        use Ecto.Schema

        @primary_key {:id, UUIDv7, autogenerate: true}
        @foreign_key_type UUIDv7
        @timestamps_opts [type: :utc_datetime_usec]
      end
    end
  end
  ```

  Avoid:

  ```elixir
  defmodule MyApp.User do
      use Ecto.Schema

      schema "users" do
        field :name, :string
      end
  end
  ```

  Prefer:

  ```elixir
  defmodule MyApp.User do
      use MyApp.Schema

      schema "users" do
        field :name, :string
      end
  end
  ```

  ## Notes

  An app schema module, such as `MyApp.Schema`, can provide project-specific
  schema defaults:
  - primary key conventions
  - foreign key conventions
  - timestamp precision and type conventions

  This check uses static AST analysis, so it favors clear source-level patterns over runtime behavior.
  Since each application chooses its own schema wrapper module, files ending in
  `/schema.ex` are treated as wrapper modules and are not reported.

  ## Options

  This check has no check-specific options. Configure it with an empty option list.

  ## Usage

  Add this check to Credo's `checks:` list in `.credo.exs`:

  ```elixir
  %{
    configs: [
      %{
        name: "default",
        checks: [
          {Bylaw.Credo.Check.Ecto.UseMyAppSchema, []}
        ]
      }
    ]
  }
  ```
  """

  use Credo.Check,
    base_priority: :higher,
    category: :design,
    explanations: [
      check: @moduledoc
    ]

  @doc false
  @impl Credo.Check
  def run(%Credo.SourceFile{} = source_file, params \\ []) do
    filename = source_file.filename

    if excluded_file?(filename) do
      []
    else
      issue_meta = IssueMeta.for(source_file, params)
      Credo.Code.prewalk(source_file, &traverse(&1, &2, issue_meta))
    end
  end

  defp excluded_file?(filename) do
    String.ends_with?(filename, "/schema.ex")
  end

  defp traverse(
         {:use, meta, [{:__aliases__, _alias_meta, [:Ecto, :Schema]} | _rest]} = ast,
         issues,
         issue_meta
       ) do
    line_no = meta[:line] || 0
    {ast, [issue_for(issue_meta, line_no) | issues]}
  end

  defp traverse(ast, issues, _issue_meta), do: {ast, issues}

  defp issue_for(issue_meta, line_no) do
    format_issue(
      issue_meta,
      message:
        "Use your app schema module, such as `use MyApp.Schema`, instead of `use Ecto.Schema`.",
      trigger: "use Ecto.Schema",
      line_no: line_no
    )
  end
end