Created
July 14, 2025 21:10
-
-
Save Nezteb/be83b29f67d43a74dd220d6e7621f7e4 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| if Code.ensure_loaded?(Credo.Check) do | |
| defmodule Credo.Check.Warning.EnumSortByFieldAccess do | |
| @moduledoc """ | |
| Custom Credo check to detect Enum.sort_by/2 calls with field access that should use Enum.sort_by/3. | |
| When sorting by any field access, you should provide a third argument to specify | |
| the comparison function for clarity and potentially better performance. | |
| ## Examples | |
| Bad: | |
| Enum.sort_by(items, & &1.name) | |
| Enum.sort_by(logs, fn log -> log.inserted_at end) | |
| Enum.sort_by(users, & &1.profile.age) | |
| Good: | |
| Enum.sort_by(items, & &1.name, :asc) | |
| Enum.sort_by(logs, fn log -> log.inserted_at end, {:desc, NaiveDateTime}) | |
| Enum.sort_by(users, & &1.profile.age, :desc) | |
| """ | |
| use Credo.Check, | |
| base_priority: :high, | |
| category: :warning, | |
| explanations: [ | |
| check: """ | |
| When using `Enum.sort_by/2` with field access, consider using `Enum.sort_by/3` | |
| and provide an explicit comparison argument as the third parameter. | |
| This makes the sorting intention explicit and can provide better performance | |
| for struct fields like DateTime, NaiveDateTime, Date, etc. | |
| ## Examples | |
| Instead of: | |
| Enum.sort_by(logs, & &1.inserted_at) | |
| Use: | |
| Enum.sort_by(logs, & &1.inserted_at, :asc) | |
| # or for struct fields: | |
| Enum.sort_by(logs, & &1.inserted_at, {:desc, NaiveDateTime}) | |
| """ | |
| ] | |
| alias Credo.Code | |
| def run(source_file, params \\ []) do | |
| issue_meta = IssueMeta.for(source_file, params) | |
| Code.prewalk(source_file, &traverse(&1, &2, issue_meta)) | |
| end | |
| # Handle pipe calls: collection |> Enum.sort_by(sort_fn) | |
| defp traverse( | |
| {:|>, _, [_collection, {{:., _, [{:__aliases__, _, [:Enum]}, :sort_by]}, meta, [sort_fn]}]} = ast, | |
| issues, | |
| issue_meta | |
| ) do | |
| if has_field_access?(sort_fn) do | |
| {ast, [issue_for(meta[:line], issue_meta) | issues]} | |
| else | |
| {ast, issues} | |
| end | |
| end | |
| # Handle direct calls: Enum.sort_by(collection, sort_fn) | |
| defp traverse( | |
| {{:., _, [{:__aliases__, _, [:Enum]}, :sort_by]}, meta, [_collection, sort_fn]} = ast, | |
| issues, | |
| issue_meta | |
| ) do | |
| if has_field_access?(sort_fn) do | |
| {ast, [issue_for(meta[:line], issue_meta) | issues]} | |
| else | |
| {ast, issues} | |
| end | |
| end | |
| defp traverse({{:., _, [{:__aliases__, _, [:Enum]}, :sort_by]}, _meta, args} = ast, issues, _issue_meta) do | |
| # For debugging only | |
| IO.puts("Unhandled Enum.sort_by:") | |
| IO.inspect(args) | |
| {ast, issues} | |
| end | |
| defp traverse(ast, issues, _issue_meta), do: {ast, issues} | |
| # This covers capture syntax, anonymous functions, and nested field access | |
| defp has_field_access?({:&, _, [{:., _, [{:&, _, [1]}, _field_name]}]}) do | |
| # Pattern: & &1.field_name (simple field access) | |
| true | |
| end | |
| defp has_field_access?({:&, _, [{{:., _, [{:&, _, [1]}, _field_name]}, _, []}]}) do | |
| # Pattern: & &1.field_name (function call syntax in AST) | |
| true | |
| end | |
| defp has_field_access?({:fn, _, [{:->, _, [{var_name, _, nil}], body}]}) do | |
| # Pattern: fn x -> ... end - check if body contains field access | |
| contains_field_access_in_body?(body, var_name) | |
| end | |
| defp has_field_access?(_), do: false | |
| # Check if the function body contains field access on the parameter | |
| defp contains_field_access_in_body?({:., _, [{var_name, _, nil}, _field]}, var_name), do: true | |
| defp contains_field_access_in_body?({{:., _, [{var_name, _, nil}, _field]}, _, _}, var_name), do: true | |
| defp contains_field_access_in_body?(_body, _var_name), do: false | |
| defp issue_for(line_no, issue_meta) do | |
| format_issue( | |
| issue_meta, | |
| message: """ | |
| Consider using Enum.sort_by/3 when sorting by field access. \ | |
| Add a third argument to specify sort direction and comparison (e.g., :asc, :desc, or {:asc, ModuleName}). | |
| """, | |
| trigger: "Enum.sort_by", | |
| line_no: line_no | |
| ) | |
| end | |
| end | |
| end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment