<commit_before>defmodule Credo.Check.Refactor.ModuleDependencies do
  @moduledoc false

  @checkdoc """
  Module is most probably doing too much. Consider limiting the number of
  module dependencies.

  As always: This is just a suggestion. Check the configuration options for
  tweaking or disabling this check.
  """
  @explanation [
    check: @checkdoc,
    params: [
      max_deps: "Maximum number of module dependencies.",
      dependency_namespaces: "List of dependency namespaces to include in this check",
      excluded_namespaces: "List of namespaces to exclude from this check",
      excluded_paths: "List of paths to exclude from this check"
    ]
  ]

  @default_params [
    max_deps: 10,
    dependency_namespaces: [],
    excluded_namespaces: [],
    excluded_paths: ["test"]
  ]

  use Credo.Check, base_priority: :normal

  alias Credo.Code.Name
  alias Credo.Code.Module

  @doc false
  def run(source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)

    max_deps = Params.get(params, :max_deps, @default_params)
    dependency_namespaces = Params.get(params, :dependency_namespaces, @default_params)
    excluded_namespaces = Params.get(params, :excluded_namespaces, @default_params)
    excluded_paths = Params.get(params, :excluded_paths, @default_params)

    case ignore_path?(source_file.filename, excluded_paths) do
      true ->
        []

      false ->
        source_file
        |> Credo.Code.prewalk(
          &traverse(
            &1,
            &2,
            issue_meta,
            dependency_namespaces,
            excluded_namespaces,
            max_deps
          )
        )
    end
  end

  defp ignore_path?(filename, excluded_paths) do
    directory = Path.dirname(filename)
    Enum.any?(excluded_paths, &String.starts_with?(directory, &1))
  end

  defp traverse(
         {:defmodule, meta, [mod | _]} = ast,
         issues,
         issue_meta,
         dependency_namespaces,
         excluded_namespaces,
         max
       ) do
    module_name = Name.full(mod)

    new_issues =
      if has_namespace?(module_name, excluded_namespaces) do
        []
      else
        module_dependencies = get_dependencies(ast, dependency_namespaces)
        issues_for_module(module_name, module_dependencies, max, issue_meta, meta)
      end

    {ast, issues ++ new_issues}
  end

  defp traverse(ast, issues, _issues_meta, _dependency_namespaces, _excluded_namespaces, _max) do
    {ast, issues}
  end

  defp get_dependencies(ast, dependency_namespaces) do
    aliases = Module.aliases(ast)

    ast
    |> Module.modules()
    |> with_fullnames(aliases)
    |> filter_namespaces(dependency_namespaces)
  end

  defp issues_for_module(mod, deps, max_deps, issue_meta, meta) when length(deps) > max_deps do
    [
      format_issue(
        issue_meta,
        message: "Module #{Name.last(mod)} has too many dependencies: #{length(deps)}",
        trigger: deps,
        line_no: meta[:line],
        column_no: meta[:column]
      )
    ]
  end

  defp issues_for_module(_, _, _, _, _), do: []

  defp with_fullnames(dependencies, aliases) do
    dependencies
    |> Enum.map(&full_name(&1, aliases))
    |> Enum.uniq()
  end

  defp filter_namespaces(dependencies, namespaces) do
    dependencies
    |> Enum.filter(&keep?(&1, namespaces))
  end

  defp keep?(_module_name, []), do: true

  defp keep?(module_name, namespaces), do: has_namespace?(module_name, namespaces)

  defp has_namespace?(module_name, namespaces) do
    namespaces
    |> Enum.any?(&String.starts_with?(module_name, &1))
  end

  # Get full module name from list of aliases (if present)
  defp full_name(dep, aliases) do
    aliases
    |> Enum.find(&String.ends_with?(&1, dep))
    |> case do
      nil -> dep
      full_name -> full_name
    end<commit_msg>Review fixes<commit_after>defmodule Credo.Check.Refactor.ModuleDependencies do
  @moduledoc false

  @checkdoc """
  This module might be doing too much. Consider limiting the number of
  module dependencies.

  As always: This is just a suggestion. Check the configuration options for
  tweaking or disabling this check.
  """
  @explanation [
    check: @checkdoc,
    params: [
      max_deps: "Maximum number of module dependencies.",
      dependency_namespaces: "List of dependency namespaces to include in this check",
      excluded_namespaces: "List of namespaces to exclude from this check",
      excluded_paths: "List of paths to exclude from this check"
    ]
  ]

  @default_params [
    max_deps: 10,
    dependency_namespaces: [],
    excluded_namespaces: [],
    excluded_paths: ["test"]
  ]

  use Credo.Check, base_priority: :normal

  alias Credo.Code.Name
  alias Credo.Code.Module

  @doc false
  def run(source_file, params \\ []) do
    issue_meta = IssueMeta.for(source_file, params)

    max_deps = Params.get(params, :max_deps, @default_params)
    dependency_namespaces = Params.get(params, :dependency_namespaces, @default_params)
    excluded_namespaces = Params.get(params, :excluded_namespaces, @default_params)
    excluded_paths = Params.get(params, :excluded_paths, @default_params)

    case ignore_path?(source_file.filename, excluded_paths) do
      true ->
        []

      false ->
        source_file
        |> Credo.Code.prewalk(
          &traverse(
            &1,
            &2,
            issue_meta,
            dependency_namespaces,
            excluded_namespaces,
            max_deps
          )
        )
    end
  end

  # Check if analyzed module path is within ignored paths
  defp ignore_path?(filename, excluded_paths) do
    directory = Path.dirname(filename)
    Enum.any?(excluded_paths, &String.starts_with?(directory, &1))
  end

  defp traverse(
         {:defmodule, meta, [mod | _]} = ast,
         issues,
         issue_meta,
         dependency_namespaces,
         excluded_namespaces,
         max
       ) do
    module_name = Name.full(mod)

    new_issues =
      if has_namespace?(module_name, excluded_namespaces) do
        []
      else
        module_dependencies = get_dependencies(ast, dependency_namespaces)
        issues_for_module(module_name, module_dependencies, max, issue_meta, meta)
      end

    {ast, issues ++ new_issues}
  end

  defp traverse(ast, issues, _issues_meta, _dependency_namespaces, _excluded_namespaces, _max) do
    {ast, issues}
  end

  defp get_dependencies(ast, dependency_namespaces) do
    aliases = Module.aliases(ast)

    ast
    |> Module.modules()
    |> with_fullnames(aliases)
    |> filter_namespaces(dependency_namespaces)
  end

  defp issues_for_module(mod, deps, max_deps, issue_meta, meta) when length(deps) > max_deps do
    [
      format_issue(
        issue_meta,
        message: "Module #{Name.last(mod)} has too many dependencies: #{length(deps)}",
        trigger: deps,
        line_no: meta[:line],
        column_no: meta[:column]
      )
    ]
  end

  defp issues_for_module(_, _, _, _, _), do: []

  # Resolve dependencies to full module names
  defp with_fullnames(dependencies, aliases) do
    dependencies
    |> Enum.map(&full_name(&1, aliases))
    |> Enum.uniq()
  end

  # Keep only dependencies which are within specified namespaces
  defp filter_namespaces(dependencies, namespaces) do
    Enum.filter(dependencies, &keep?(&1, namespaces))
  end

  defp keep?(_module_name, []), do: true

  defp keep?(module_name, namespaces), do: has_namespace?(module_name, namespaces)

  defp has_namespace?(module_name, namespaces) do
    Enum.any?(namespaces, &String.starts_with?(module_name, &1))
  end

  # Get full module name from list of aliases (if present)
  defp full_name(dep, aliases) do
    aliases
    |> Enum.find(&String.ends_with?(&1, dep))
    |> case do
      nil -> dep
      full_name -> full_name
    end
{'fraction_non_alphanumeric': 0.09672447013487476, 'fraction_numerical': 0.0023121387283236996, 'mean_word_length': 3.001027749229188, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '406841', 'n_tokens_mistral': 2491, 'n_tokens_neox': 2415, 'n_words': 595}