defmodule Bylaw.Credo.Check.Ecto.PreferDateTimeOverDate do
@moduledoc """
Prefer `:naive_datetime` or `:utc_datetime` over `:date` in Ecto schemas and
migrations when you need precise timestamps.
## Examples
Avoid:
schema "events" do
field :starts_on, :date
end
alter table(:events) do
add :starts_on, :date
modify :ends_on, :date
end
Prefer:
schema "events" do
field :starts_at, :utc_datetime
end
alter table(:events) do
add :starts_at, :utc_datetime
timestamps(type: :utc_datetime)
end
## Notes
Use `:naive_datetime`, `:utc_datetime`, or `timestamps/1` unless the field is
intentionally a calendar-only value. If a true date-only field is required,
disable the check locally with Credo.
This check uses static AST analysis, so it favors clear source-level patterns over runtime behavior.
## 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.PreferDateTimeOverDate, []}
]
}
]
}
```
"""
use Credo.Check,
base_priority: :high,
category: :warning,
explanations: [
check: @moduledoc
]
@schema_macros [:schema, :embedded_schema]
@migration_operations [:add, :add_if_not_exists, :modify]
@date_type :date
@doc false
@impl Credo.Check
def run(source_file, params \\ []) do
ctx = Context.build(source_file, params, __MODULE__)
source_file
|> Credo.Code.prewalk(&walk_schema/2, ctx)
|> maybe_check_migration_columns(source_file)
|> Map.get(:issues, [])
end
defp walk_schema({macro, _meta, [[do: block]]} = ast, ctx) when macro in @schema_macros do
{ast, find_schema_date_fields(block, ctx)}
end
defp walk_schema({macro, _meta, [_source, [do: block]]} = ast, ctx)
when macro in @schema_macros do
{ast, find_schema_date_fields(block, ctx)}
end
defp walk_schema(ast, ctx), do: {ast, ctx}
defp find_schema_date_fields(block, ctx) do
block
|> Macro.prewalk(ctx, fn
{:field, meta, [_name, @date_type | _rest]} = ast, acc ->
{ast, put_issue(acc, issue_for(acc, meta, "field", "schema field"))}
ast, acc ->
{ast, acc}
end)
|> elem(1)
end
defp maybe_check_migration_columns(ctx, source_file) do
if migration_file?(source_file) do
Credo.Code.prewalk(source_file, &walk_migration/2, ctx)
else
ctx
end
end
defp walk_migration({operation, meta, [_name, @date_type | _rest]} = ast, ctx)
when operation in @migration_operations do
{ast, put_issue(ctx, issue_for(ctx, meta, Atom.to_string(operation), "migration column"))}
end
defp walk_migration(ast, ctx), do: {ast, ctx}
defp migration_file?(source_file) do
String.contains?(source_file.filename, "/priv/repo/migrations/") or
String.starts_with?(source_file.filename, "priv/repo/migrations/")
end
defp issue_for(ctx, meta, trigger, subject) do
format_issue(
ctx,
message: "Prefer `:naive_datetime` or `:utc_datetime` over `:date` for #{subject}.",
trigger: trigger,
line_no: meta[:line]
)
end
end