diff --git a/lib/credo/check/readability/strict_module_layout.ex b/lib/credo/check/readability/strict_module_layout.ex index c99d05050..4654db557 100644 --- a/lib/credo/check/readability/strict_module_layout.ex +++ b/lib/credo/check/readability/strict_module_layout.ex @@ -61,12 +61,20 @@ defmodule Credo.Check.Readability.StrictModuleLayout do List of atoms identifying the module parts which are not checked, and may therefore appear anywhere in the module. Supported values are the same as in the `:order` param. + """, + ignore_module_attributes: """ + List of atoms identifying the module attributes which are not checked, and may + therefore appear anywhere in the module. Useful for custom DSLs that use attributes + before function heads. + + For example, if you provide `~w/trace/a`, all `@trace` attributes will be ignored. """ ] ], param_defaults: [ order: ~w/shortdoc moduledoc behaviour use import alias require/a, - ignore: [] + ignore: [], + ignore_module_attributes: [] ] alias Credo.CLI.Output.UI @@ -106,6 +114,7 @@ defmodule Credo.Check.Readability.StrictModuleLayout do defp all_errors(modules_and_parts, params, issue_meta) do expected_order = expected_order(params) ignored_parts = Keyword.get(params, :ignore, []) + ignore_module_attributes = Keyword.get(params, :ignore_module_attributes, []) Enum.reduce( modules_and_parts, @@ -118,13 +127,25 @@ defmodule Credo.Check.Readability.StrictModuleLayout do # because enforcing an internal order between these two kinds is counterproductive if # a module implements multiple behaviours. In such cases, we typically want to group # callbacks by the implementation, not by the kind (fun vs macro). - {callback_impl, location} when callback_impl in ~w/callback_macro callback_fun/a -> - {:callback_impl, location} + {callback_impl, meta} when callback_impl in ~w/callback_macro callback_fun/a -> + {:callback_impl, meta} other -> other end) - |> Stream.reject(fn {part, _location} -> part in ignored_parts end) + |> Stream.reject(fn {part, meta} -> + cond do + part in ignored_parts -> + true + + part == :module_attribute and + Keyword.get(meta, :attribute, nil) in ignore_module_attributes -> + true + + true -> + false + end + end) module_errors(module, parts, expected_order, issue_meta) ++ errors end diff --git a/lib/credo/code/module.ex b/lib/credo/code/module.ex index 9651eda5b..5cb29cc33 100644 --- a/lib/credo/code/module.ex +++ b/lib/credo/code/module.ex @@ -371,6 +371,9 @@ defmodule Credo.Code.Module do typedoc dialyzer external_resource file on_definition on_load vsn spec/a, do: state + defp analyze(state, {:@, meta, [{name, _, _}]}), + do: add_module_element(state, :module_attribute, Keyword.put(meta, :attribute, name)) + defp analyze(state, {:@, meta, _}), do: add_module_element(state, :module_attribute, meta) @@ -444,7 +447,7 @@ defmodule Credo.Code.Module do end defp add_module_element(state, element, meta) do - location = Keyword.take(meta, ~w/line column/a) - update_in(state.current_module.parts, &[{element, location} | &1]) + meta = Keyword.take(meta, ~w/attribute line column/a) + update_in(state.current_module.parts, &[{element, meta} | &1]) end end diff --git a/test/credo/check/readability/strict_module_layout_test.exs b/test/credo/check/readability/strict_module_layout_test.exs index 76deb19be..5bbad7821 100644 --- a/test/credo/check/readability/strict_module_layout_test.exs +++ b/test/credo/check/readability/strict_module_layout_test.exs @@ -277,4 +277,60 @@ defmodule Credo.Check.Readability.StrictModuleLayoutTest do assert issue.message == "alias must appear before require" end end + + describe "ignored module attributes" do + test "ignores custom module attributes" do + """ + defmodule Test do + use Baz + + import Bar + + @trace trace_fun() + def test_fun() do + nil + end + + @trace trace_fun() + def test() do + nil + end + end + """ + |> to_source_file + |> run_check(@described_check, + order: ~w(use import module_attribute)a, + ignore_module_attributes: ~w/trace/a + ) + |> refute_issues + end + + test "only ignores set module attributes" do + [issue] = + """ + defmodule Test do + import Bar + + @trace trace_fun() + def test_fun() do + nil + end + + @bad_attribute + @trace trace_fun() + def test() do + nil + end + end + """ + |> to_source_file + |> run_check(@described_check, + order: ~w(import module_attribute)a, + ignore_module_attributes: ~w/trace/a + ) + |> assert_issue + + assert issue.message == "module attribute must appear before public function" + end + end end diff --git a/test/credo/code/module_test.exs b/test/credo/code/module_test.exs index 2bca40896..7acf04f31 100644 --- a/test/credo/code/module_test.exs +++ b/test/credo/code/module_test.exs @@ -429,7 +429,7 @@ defmodule Credo.Code.ModuleTest do end test "recognizes module attribute" do - assert analyze(~s/@mod_attr 1/) == [{Test, [module_attribute: [line: 2, column: 3]]}] + assert analyze(~s/@mod_attr 1/) == [{Test, [module_attribute: [attribute: :mod_attr, line: 2, column: 3]]}] end test "recognizes struct definition" do