diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/credo/check/consistency/file_location.ex | 166 | 
1 files changed, 166 insertions, 0 deletions
| diff --git a/test/credo/check/consistency/file_location.ex b/test/credo/check/consistency/file_location.ex new file mode 100644 index 000000000..500983608 --- /dev/null +++ b/test/credo/check/consistency/file_location.ex @@ -0,0 +1,166 @@ +# Pleroma: A lightweight social networking server +# Originally taken from +# https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Credo.Check.Consistency.FileLocation do +  @moduledoc false + +  # credo:disable-for-this-file Credo.Check.Readability.Specs + +  @checkdoc """ +  File location should follow the namespace hierarchy of the module it defines. + +  Examples: + +      - `lib/my_system.ex` should define the `MySystem` module +      - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module +  """ +  @explanation [warning: @checkdoc] + +  @special_namespaces [ +    "controllers", +    "views", +    "operations", +    "channels" +  ] + +  # `use Credo.Check` required that module attributes are already defined, so we need +  # to place these attributes +  # before use/alias expressions. +  # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout +  use Credo.Check, category: :warning, base_priority: :high + +  alias Credo.Code + +  def run(source_file, params \\ []) do +    case verify(source_file, params) do +      :ok -> +        [] + +      {:error, module, expected_file} -> +        error(IssueMeta.for(source_file, params), module, expected_file) +    end +  end + +  defp verify(source_file, params) do +    source_file.filename +    |> Path.relative_to_cwd() +    |> verify(Code.ast(source_file), params) +  end + +  @doc false +  def verify(relative_path, ast, params) do +    if verify_path?(relative_path, params), +      do: ast |> main_module() |> verify_module(relative_path, params), +      else: :ok +  end + +  defp verify_path?(relative_path, params) do +    case Path.split(relative_path) do +      ["lib" | _] -> not exclude?(relative_path, params) +      ["test", "support" | _] -> false +      ["test", "test_helper.exs"] -> false +      ["test" | _] -> not exclude?(relative_path, params) +      _ -> false +    end +  end + +  defp exclude?(relative_path, params) do +    params +    |> Keyword.get(:exclude, []) +    |> Enum.any?(&String.starts_with?(relative_path, &1)) +  end + +  defp main_module(ast) do +    {_ast, modules} = Macro.prewalk(ast, [], &traverse/2) +    Enum.at(modules, -1) +  end + +  defp traverse({:defmodule, _meta, args}, modules) do +    [{:__aliases__, _, name_parts}, _module_body] = args +    {args, [Module.concat(name_parts) | modules]} +  end + +  defp traverse(ast, state), do: {ast, state} + +  # empty file - shouldn't really happen, but we'll let it through +  defp verify_module(nil, _relative_path, _params), do: :ok + +  defp verify_module(main_module, relative_path, params) do +    parsed_path = parsed_path(relative_path, params) + +    expected_file = +      expected_file_base(parsed_path.root, main_module) <> +        Path.extname(parsed_path.allowed) + +    cond do +      expected_file == parsed_path.allowed -> +        :ok + +      special_namespaces?(parsed_path.allowed) -> +        original_path = parsed_path.allowed + +        namespace = +          Enum.find(@special_namespaces, original_path, fn namespace -> +            String.contains?(original_path, namespace) +          end) + +        allowed = String.replace(original_path, "/" <> namespace, "") + +        if expected_file == allowed, +          do: :ok, +          else: {:error, main_module, expected_file} + +      true -> +        {:error, main_module, expected_file} +    end +  end + +  defp special_namespaces?(path), do: String.contains?(path, @special_namespaces) + +  defp parsed_path(relative_path, params) do +    parts = Path.split(relative_path) + +    allowed = +      Keyword.get(params, :ignore_folder_namespace, %{}) +      |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end) +      |> Stream.map(&Path.split/1) +      |> Enum.find(&List.starts_with?(parts, &1)) +      |> case do +        nil -> +          relative_path + +        ignore_parts -> +          Stream.drop(ignore_parts, -1) +          |> Enum.concat(Stream.drop(parts, length(ignore_parts))) +          |> Path.join() +      end + +    %{root: hd(parts), allowed: allowed} +  end + +  defp expected_file_base(root_folder, module) do +    {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1) + +    relative_path = +      if parent_namespace == [], +        do: "", +        else: parent_namespace |> Module.concat() |> Macro.underscore() + +    file_name = module_name |> Module.concat() |> Macro.underscore() + +    Path.join([root_folder, relative_path, file_name]) +  end + +  defp error(issue_meta, module, expected_file) do +    format_issue(issue_meta, +      message: +        "Mismatch between file name and main module #{inspect(module)}. " <> +          "Expected file path to be #{expected_file}. " <> +          "Either move the file or rename the module.", +      line_no: 1 +    ) +  end +end | 
