Packages
credo
1.7.0
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.7-rc.0
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.2-rc.4
1.7.2-rc.3
1.7.2-rc.2
1.7.2-rc.1
1.7.2-rc.0
1.7.1
1.7.0
1.7.0-rc.2
1.7.0-rc.1
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.6.0-rc.1
1.6.0-rc.0
1.5.6
1.5.5
1.5.4
1.5.3
1.5.2
1.5.1
1.5.0
1.5.0-rc.5
1.5.0-rc.4
1.5.0-rc.3
1.5.0-rc.2
1.5.0-rc.1
1.4.1
1.4.0
1.4.0-rc.2
1.4.0-rc.1
1.3.2
1.3.1
1.3.0
1.3.0-rc3
1.3.0-rc2
1.3.0-rc1
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc4
1.2.0-rc3
1.2.0-rc2
1.2.0-rc1
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc3
1.1.0-rc2
1.1.0-rc1
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.1-rc1
1.0.0
1.0.0-rc1
0.10.2
0.10.1
0.10.0
0.9.3
0.9.2
0.9.1
0.9.0
0.9.0-rc8
0.9.0-rc7
0.9.0-rc6
0.9.0-rc5
0.9.0-rc4
0.9.0-rc3
0.9.0-rc2
0.9.0-rc1
0.8.10
0.8.9
0.8.8
0.8.7
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.8.0-rc7
0.8.0-rc6
0.8.0-rc5
0.8.0-rc4
0.8.0-rc3
0.8.0-rc2
0.8.0-rc1
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.6.0-rc2
0.6.0-rc1
0.5.3
0.5.2
0.5.1
0.5.0
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.10-dev
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.4.0-beta5
0.4.0-beta4
0.4.0-beta3
0.4.0-beta2
0.4.0-beta1
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.3.0-dev2
0.3.0-dev
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.10
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.1-dev
A static code analysis tool with a focus on code consistency and teaching.
Current section
Files
Jump to
Current section
Files
lib/credo/check/consistency/collector.ex
defmodule Credo.Check.Consistency.Collector do
@moduledoc """
A behavior for modules that walk through source files and
identify consistency issues.
When defining a consistency check, you would typically use
this structure for the main module, responsible
for formatting issue messages:
defmodule Credo.Check.Consistency.SomeCheck do
use Credo.Check, run_on_all: true
@collector Credo.Check.Consistency.SomeCheck.Collector
def run(source_files, exec, params) when is_list(source_files) do
issue_formatter = &issues_for/3
@collector.find_and_append_issues(source_files, exec, params, issue_formatter)
end
defp issues_for(expected, source_file, params) do
issue_meta = IssueMeta.for(source_file, params)
issue_locations =
@collector.find_locations_not_matching(expected, source_file)
Enum.map(issue_locations, fn(location) ->
format_issue issue_meta, message: ... # write an issue message
end)
end
The actual analysis would be performed by another module
implementing the `Credo.Check.Consistency.Collector` behavior:
defmodule Credo.Check.Consistency.SomeCheck.Collector do
use Credo.Check.Consistency.Collector
def collect_matches(source_file, params) do
# ...
end
def find_locations_not_matching(expected, source_file) do
# ...
end
end
Read further for more information on `collect_matches/2`,
`find_locations_not_matching/2`, and `issue_formatter`.
"""
alias Credo.Execution.ExecutionIssues
alias Credo.Issue
alias Credo.SourceFile
@doc """
When you call `@collector.find_and_append_issues/4` inside the check module,
the collector first counts the occurrences of different matches
(e.g. :with_space and :without_space for a space around operators check)
per each source file.
`collect_matches/2` produces a map of matches as keys and their frequencies
as values (e.g. %{with_space: 50, without_space: 40}).
The maps for individual source files are then merged, producing a map
that reflects frequency trends for the whole codebase.
"""
@callback collect_matches(
source_file :: SourceFile.t(),
params :: Keyword.t()
) :: %{
term => non_neg_integer
}
# Once the most frequent match is identified, the `Collector` looks up
# source files that have other matches (e.g. both :with_space
# and :without_space or just :without_space when :with_space is the
# most frequent) and calls the `issue_formatter` function on them.
#
# An issue formatter produces a list of `Credo.Issue` structs
# from the most frequent (expected) match, a source file
# containing other matches, and check params
# (the latter two are required to build an IssueMeta).
@type issue_formatter :: (term, SourceFile.t(), Keyword.t() -> [Issue.t()])
@doc """
`issue_formatter` may call the `@collector.find_locations_not_matching/2`
function to obtain additional metadata for each occurrence of
an unexpected match in a given file.
An example implementation that returns a list of line numbers on
which unexpected occurrences were found:
def find_locations_not_matching(expected, source_file) do
traverse(source_file, fn(match, line_no, acc) ->
if match != expected do
acc ++ [line_no]
else
acc
end
end)
end
defp traverse(source_file, fun), do: ...
"""
@callback find_locations_not_matching(
expected :: term,
source_file :: SourceFile.t()
) :: list(term)
@optional_callbacks find_locations_not_matching: 2
defmacro __using__(_opts) do
quote do
@behaviour Credo.Check.Consistency.Collector
alias Credo.Check.Consistency.Collector
alias Credo.Execution
alias Credo.Issue
alias Credo.SourceFile
@spec find_and_append_issues(
[SourceFile.t()],
Execution.t(),
Keyword.t(),
Collector.issue_formatter(),
boolean()
) :: atom
def find_and_append_issues(
source_files,
exec,
params,
issue_formatter,
supress_issues_for_single_match? \\ false
)
when is_list(source_files) and is_function(issue_formatter) do
source_files
|> Collector.find_issues(
__MODULE__,
params,
issue_formatter,
supress_issues_for_single_match?
)
|> Enum.each(&Collector.append_issue_via_issue_service(&1, exec))
:ok
end
end
end
def find_issues(
source_files,
collector,
params,
issue_formatter,
supress_issues_for_single_match?
) do
frequencies_per_source_file =
source_files
|> Enum.map(&Task.async(fn -> {&1, collector.collect_matches(&1, params)} end))
|> Enum.map(&Task.await(&1, :infinity))
frequencies = total_frequencies(frequencies_per_source_file)
if map_size(frequencies) > 0 do
most_frequent_match =
most_frequent_match(frequencies, supress_issues_for_single_match?, params[:force])
result =
frequencies_per_source_file
|> source_files_with_issues(most_frequent_match)
|> Enum.map(&Task.async(fn -> issue_formatter.(most_frequent_match, &1, params) end))
|> Enum.flat_map(&Task.await(&1, :infinity))
result
else
[]
end
end
defp most_frequent_match(frequencies, supress_issues_for_single_match?, nil) do
{value, frequency_of_match} = Enum.max_by(frequencies, &elem(&1, 1))
single_match? = frequency_of_match == 1
if single_match? && supress_issues_for_single_match? do
:__only_single_match__
else
value
end
end
defp most_frequent_match(_frequencies, _supress_issues_for_single_match, forced_value) do
forced_value
end
def append_issue_via_issue_service(%Issue{} = issue, exec) do
ExecutionIssues.append(exec, issue)
end
defp source_files_with_issues(_frequencies_per_file, :__only_single_match__) do
[]
end
defp source_files_with_issues(frequencies_per_file, most_frequent_match) do
Enum.reduce(frequencies_per_file, [], fn {filename, stats}, acc ->
unexpected_matches = Map.keys(stats) -- [most_frequent_match]
if unexpected_matches != [] do
[filename | acc]
else
acc
end
end)
end
defp total_frequencies(frequencies_per_file) do
Enum.reduce(frequencies_per_file, %{}, fn {_, file_stats}, stats ->
Map.merge(stats, file_stats, fn _k, f1, f2 -> f1 + f2 end)
end)
end
end