Current section
Files
Jump to
Current section
Files
lib/explico/check/vale.ex
defmodule Explico.Check.Vale do
use Credo.Check,
run_on_all: true,
category: :readability,
base_priority: :normal,
elixir_version: ">= 1.14.0",
param_defaults: [vale_config: nil, vale_executable: "vale"],
explanations: [
check: """
Runs Vale over documentation extracted from Credo's selected Elixir source files.
Add this check to the `:extra` checks in a configuration in your project's
`.credo.exs`. When the Vale executable is not found, the check is skipped.
Configuration and execution failures are reported as Credo issues.
""",
params: [
vale_config: "Path to a Vale configuration file, relative to the project root.",
vale_executable: "Executable name or absolute path."
]
]
alias Credo.Check.Params
alias Credo.{Check, Issue, Priority, SourceFile}
alias Explico.{Extractor, Vale}
@impl true
def run_on_all_source_files(exec, source_files, params) do
issues = issues_for_source_files(source_files, params)
append_issues_and_timings(issues, exec)
:ok
end
@doc false
def issues_for_source_files(source_files, params \\ []) do
root = Keyword.get(params, :project_root, File.cwd!())
extractor = Keyword.get(params, :extractor, Extractor)
vale = Keyword.get(params, :vale, Vale)
executable = Keyword.get(params, :vale_executable, "vale")
if vale_available?(vale, executable, params) do
documents = extract_documents(source_files, extractor)
run_vale(documents, vale, executable, root, params)
else
[]
end
rescue
error -> [unavailable_issue(Exception.message(error), root_for(params), params)]
end
defp extract_documents(source_files, extractor) do
Enum.flat_map(source_files, fn source_file ->
{source, filename} = source_and_filename(source_file)
case extractor.extract_source(source, filename) do
{:ok, documents} -> documents
{:error, _reason} -> []
end
end)
end
defp run_vale([], _vale, _executable, _root, _params), do: []
defp run_vale(documents, vale, executable, root, params) do
options =
[executable: executable, config: params[:vale_config], cwd: root]
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> maybe_add_command_runner(params)
case vale.check(documents, options) do
{:ok, diagnostics} -> Enum.map(diagnostics, &issue_for(&1, params))
{:error, reason} -> [unavailable_issue(reason, root, params)]
end
end
defp source_and_filename(%SourceFile{} = source_file),
do: SourceFile.source_and_filename(source_file)
defp source_and_filename(%{source: source, filename: filename}), do: {source, filename}
defp vale_available?(Vale, executable, params) do
checker = Keyword.get(params, :vale_available?, &default_vale_available?/1)
checker.(executable)
end
defp vale_available?(_injected_vale, _executable, _params), do: true
defp default_vale_available?(executable) do
(Path.type(executable) == :absolute and File.regular?(executable)) or
not is_nil(System.find_executable(executable))
end
defp maybe_add_command_runner(options, params) do
case Keyword.fetch(params, :command_runner) do
{:ok, runner} -> Keyword.put(options, :command_runner, runner)
:error -> options
end
end
defp issue_for(diagnostic, params) do
category = configured_category(params, category_for(diagnostic.severity))
%Issue{
category: category,
check: __MODULE__,
exit_status: configured_exit_status(params, category),
filename: diagnostic.file,
line_no: diagnostic.line,
message: "[#{diagnostic.check}] #{diagnostic.message}",
meta: [vale_check: diagnostic.check],
priority: configured_priority(params, diagnostic.severity),
scope: :module,
severity: diagnostic.severity,
trigger: diagnostic.text || diagnostic.check
}
end
defp unavailable_issue(reason, root, params) do
category = configured_category(params, :warning)
%Issue{
category: category,
check: __MODULE__,
exit_status: configured_exit_status(params, category),
filename: Path.join(root, "mix.exs"),
line_no: 1,
message: "Vale documentation check unavailable: #{reason}",
meta: [vale_error: reason],
priority: configured_priority(params, :warning),
severity: :warning,
trigger: Issue.no_trigger()
}
end
defp configured_category(params, default) do
if params[:__category__] || params[:category],
do: Params.category(params, __MODULE__),
else: default
end
defp configured_exit_status(params, category) do
case params[:__exit_status__] || params[:exit_status] do
nil -> Check.to_exit_status(category)
_configured -> params |> Params.exit_status(__MODULE__) |> Check.to_exit_status()
end
end
defp configured_priority(params, severity) do
case params[:__priority__] || params[:priority] do
nil -> priority_for(severity)
configured -> Priority.to_integer(configured)
end
end
defp root_for(params), do: Keyword.get(params, :project_root, File.cwd!())
defp category_for(:suggestion), do: :refactor
defp category_for(_severity), do: :warning
defp priority_for(:error), do: Priority.to_integer(:high)
defp priority_for(:warning), do: Priority.to_integer(:normal)
defp priority_for(:suggestion), do: Priority.to_integer(:low)
end