Current section
Files
Jump to
Current section
Files
lib/warnex.ex
defmodule Warnex do
@moduledoc """
This module is meant to assist in ordering the warnings outputted by this application.
Initially, you must gather the warnings from the compilation output:
```
rm warnings.log; mix compile --force --all-warnings > warnings.log 2>&1
OR
alias Warnex
Warnex.generate_warnings()
```
"""
@doc """
Runs the compilation process and generates the warnings.log file.
Returns :ok if successful, {:error, reason} if the command fails.
## Examples
iex> Warnex.generate_warnings()
:ok
"""
def generate_warnings do
case System.cmd("sh", [
"-c",
"rm warnings.log; mix compile --force --all-warnings > warnings.log 2>&1"
]) do
{_, 0} -> :ok
{error, _} -> {:error, error}
end
end
## Processing ##
@doc """
Describes the total warnings for all files that have warnings
## Examples
iex(1)> Warnex.summary
[
["shared_view.ex", 1],
["page_controller.ex", 1],
["items.ex", 1],
]
"""
def summary() do
Enum.group_by(parse_warnings(), fn map ->
Map.get(map[:location] || %{}, :filename, "unknown")
end)
|> Enum.map(fn {filename, warnings} ->
[filename, warnings |> Enum.count()]
end)
|> Enum.sort_by(fn [_head, tail] -> tail end)
|> Enum.reverse()
end
@doc """
Gives you the next n file warnings
## Examples
iex(1)> Warnex.next(5)
[
[
%{
message: "missing parentheses for expression following \"else:\" keyword. Parentheses are required to solve ambiguity inside keywords.",
location: %{
line: 11,
filename: "shared_view.ex",
path: "lib/application_web/views"
}
}
],
[...],
...
]
"""
def next(n) do
summary()
|> Enum.take(n)
|> Enum.map(fn [filename, _] -> for_file(filename) end)
end
@doc """
Shows all warning counts for all files with warnings
## Examples
iex(1)> Warnex.count
8
"""
def count do
parse_warnings() |> Enum.count()
end
@doc """
Returns the total amount of errors for a specific file
## Example
iex(1)> Warnex.count("shared_view.ex")
1
"""
def count(lookup) do
for_file(lookup) |> Enum.count()
end
@doc """
Returns the warnings for a specific file lookup
## Examples
iex(1)> Warnex.for_file("shared_view.ex")
[
%{
message: "missing parentheses for expression following \"else:\" keyword. Parentheses are required to solve ambiguity inside keywords.",
location: %{
line: 11,
filename: "shared_view.ex",
path: "lib/application_web/views"
}
},
%{...},
...
]
"""
def for_file(lookup) do
parse_warnings()
|> Enum.filter(fn %{message: _, location: %{path: _path, filename: filename, line: _line}} ->
lookup == filename
end)
end
@doc """
Returns all warnings where the existing warning contains the given string
## Examples
iex(1)> Warnex.for_warning("missing parentheses for expression")
%{
"shared_view.ex" => [
%{
message: "missing parentheses for expression following \"else:\" keyword. Parentheses are required to solve ambiguity inside keywords.",
location: %{
line: 11,
filename: "shared_view.ex",
path: "lib/application_web/views"
}
}
],
"..." => [...]
}
"""
def for_warning(warning) do
parse_warnings()
|> Enum.filter(fn %{
message: message,
location: %{path: _path, filename: _filename, line: _line}
} ->
message |> String.contains?(warning)
end)
|> Enum.group_by(fn %{
message: _message,
location: %{path: _path, filename: filename, line: _line}
} ->
filename
end)
end
## Parsing ##
@doc """
Parses the log output of a compilation into maps
## Examples
iex(1)> Warnex.parse_warnings()
[
%{
message: "missing parentheses for expression following \"else:\" keyword. Parentheses are required to solve ambiguity inside keywords.",
location: %{
line: 11,
filename: "shared_view.ex",
path: "lib/application_web/views"
}
},
%{...},
...
]
"""
def parse_warnings do
"warnings.log"
|> File.read!()
# Split by new warnings
|> String.split(~r/^warning:/m)
# Parses block for warning
|> Enum.map(&parse_warning_block/1)
# Remove nil entries (invalid warnings)
|> Enum.reject(&is_nil/1)
|> List.flatten()
end
defp parse_warning_block(block) do
block
|> String.trim()
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.reject(&String.starts_with?(&1, "Compiling"))
|> Enum.reject(&String.starts_with?(&1, "Generated"))
|> case do
[head | tail] ->
# Parse the locations into individual lines
Enum.map(parse_locations(tail), fn {path, filename, line} ->
%{
message: head |> String.replace(",", " "),
location: %{path: path, filename: filename, line: line}
}
end)
_ ->
nil
end
end
defp parse_locations(lines) do
lines
|> Enum.filter(&String.contains?(&1, ":"))
|> Enum.map(&parse_location_line/1)
|> Enum.reject(&is_nil/1)
end
defp parse_location_line(location) do
case String.split(location, ":", parts: 3) do
[path, line_number | _rest] ->
case Integer.parse(line_number) do
{number, _} ->
[filename | tail] = String.split(path, "/") |> Enum.reverse()
{tail |> Enum.reverse() |> Enum.join("/") |> String.trim(), filename, number}
:error ->
nil
end
_ ->
nil
end
end
end