logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma git clone https://hacktivis.me/git/pleroma.git

file_location.ex (4856B)


  1. # Pleroma: A lightweight social networking server
  2. # Originally taken from
  3. # https://github.com/VeryBigThings/elixir_common/blob/master/lib/vbt/credo/check/consistency/file_location.ex
  4. # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
  5. # SPDX-License-Identifier: AGPL-3.0-only
  6. defmodule Credo.Check.Consistency.FileLocation do
  7. @moduledoc false
  8. # credo:disable-for-this-file Credo.Check.Readability.Specs
  9. @checkdoc """
  10. File location should follow the namespace hierarchy of the module it defines.
  11. Examples:
  12. - `lib/my_system.ex` should define the `MySystem` module
  13. - `lib/my_system/accounts.ex` should define the `MySystem.Accounts` module
  14. """
  15. @explanation [warning: @checkdoc]
  16. @special_namespaces [
  17. "controllers",
  18. "views",
  19. "operations",
  20. "channels"
  21. ]
  22. # `use Credo.Check` required that module attributes are already defined, so we need
  23. # to place these attributes
  24. # before use/alias expressions.
  25. # credo:disable-for-next-line VBT.Credo.Check.Consistency.ModuleLayout
  26. use Credo.Check, category: :warning, base_priority: :high
  27. alias Credo.Code
  28. def run(source_file, params \\ []) do
  29. case verify(source_file, params) do
  30. :ok ->
  31. []
  32. {:error, module, expected_file} ->
  33. error(IssueMeta.for(source_file, params), module, expected_file)
  34. end
  35. end
  36. defp verify(source_file, params) do
  37. source_file.filename
  38. |> Path.relative_to_cwd()
  39. |> verify(Code.ast(source_file), params)
  40. end
  41. @doc false
  42. def verify(relative_path, ast, params) do
  43. if verify_path?(relative_path, params),
  44. do: ast |> main_module() |> verify_module(relative_path, params),
  45. else: :ok
  46. end
  47. defp verify_path?(relative_path, params) do
  48. case Path.split(relative_path) do
  49. ["lib" | _] -> not exclude?(relative_path, params)
  50. ["test", "support" | _] -> false
  51. ["test", "test_helper.exs"] -> false
  52. ["test" | _] -> not exclude?(relative_path, params)
  53. _ -> false
  54. end
  55. end
  56. defp exclude?(relative_path, params) do
  57. params
  58. |> Keyword.get(:exclude, [])
  59. |> Enum.any?(&String.starts_with?(relative_path, &1))
  60. end
  61. defp main_module(ast) do
  62. {_ast, modules} = Macro.prewalk(ast, [], &traverse/2)
  63. Enum.at(modules, -1)
  64. end
  65. defp traverse({:defmodule, _meta, args}, modules) do
  66. [{:__aliases__, _, name_parts}, _module_body] = args
  67. {args, [Module.concat(name_parts) | modules]}
  68. end
  69. defp traverse(ast, state), do: {ast, state}
  70. # empty file - shouldn't really happen, but we'll let it through
  71. defp verify_module(nil, _relative_path, _params), do: :ok
  72. defp verify_module(main_module, relative_path, params) do
  73. parsed_path = parsed_path(relative_path, params)
  74. expected_file =
  75. expected_file_base(parsed_path.root, main_module) <>
  76. Path.extname(parsed_path.allowed)
  77. cond do
  78. expected_file == parsed_path.allowed ->
  79. :ok
  80. special_namespaces?(parsed_path.allowed) ->
  81. original_path = parsed_path.allowed
  82. namespace =
  83. Enum.find(@special_namespaces, original_path, fn namespace ->
  84. String.contains?(original_path, namespace)
  85. end)
  86. allowed = String.replace(original_path, "/" <> namespace, "")
  87. if expected_file == allowed,
  88. do: :ok,
  89. else: {:error, main_module, expected_file}
  90. true ->
  91. {:error, main_module, expected_file}
  92. end
  93. end
  94. defp special_namespaces?(path), do: String.contains?(path, @special_namespaces)
  95. defp parsed_path(relative_path, params) do
  96. parts = Path.split(relative_path)
  97. allowed =
  98. Keyword.get(params, :ignore_folder_namespace, %{})
  99. |> Stream.flat_map(fn {root, folders} -> Enum.map(folders, &Path.join([root, &1])) end)
  100. |> Stream.map(&Path.split/1)
  101. |> Enum.find(&List.starts_with?(parts, &1))
  102. |> case do
  103. nil ->
  104. relative_path
  105. ignore_parts ->
  106. Stream.drop(ignore_parts, -1)
  107. |> Enum.concat(Stream.drop(parts, length(ignore_parts)))
  108. |> Path.join()
  109. end
  110. %{root: hd(parts), allowed: allowed}
  111. end
  112. defp expected_file_base(root_folder, module) do
  113. {parent_namespace, module_name} = module |> Module.split() |> Enum.split(-1)
  114. relative_path =
  115. if parent_namespace == [],
  116. do: "",
  117. else: parent_namespace |> Module.concat() |> Macro.underscore()
  118. file_name = module_name |> Module.concat() |> Macro.underscore()
  119. Path.join([root_folder, relative_path, file_name])
  120. end
  121. defp error(issue_meta, module, expected_file) do
  122. format_issue(issue_meta,
  123. message:
  124. "Mismatch between file name and main module #{inspect(module)}. " <>
  125. "Expected file path to be #{expected_file}. " <>
  126. "Either move the file or rename the module.",
  127. line_no: 1
  128. )
  129. end
  130. end