Packages

A Phoenix/Elixir application warning manager that helps track and manage application warnings effectively.

Current section

Files

Jump to
warnex lib warnex.ex
Raw

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: "variable \"result\" is unused (if the variable is not meant to be used, prefix it with an underscore)",
location: %{
line: 44,
filename: "recurring_call_job_status_cache.ex",
path: "lib/core_blayze_app/cache"
}
},
%{...},
...
]
"""
def parse_warnings do
"warnings.log"
|> File.read!()
# Split by new warnings, but keep the "warning:" prefix for validation
|> String.split("warning: ")
# Remove the initial "Compiling" lines
|> Enum.reject(&String.starts_with?(&1, "Compiling"))
# Parse each warning block
|> 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
# Split the block into lines and remove empty lines
lines =
block
|> String.split("\n")
|> Enum.reject(&(&1 == ""))
|> Enum.reject(&String.starts_with?(&1, "Generated"))
case lines do
# Skip empty blocks
[] ->
nil
# Process valid warning blocks
_ ->
# Extract the message (first line)
message =
lines
|> List.first()
|> String.replace(~r/^warning:\s*/, "")
|> String.trim()
# Find the location line (usually starts with "└─" or contains a file path)
location =
lines
|> Enum.find(&(String.contains?(&1, "└─") || String.contains?(&1, ".ex:")))
case parse_location(location) do
nil -> nil
loc -> %{message: message, location: loc}
end
end
end
defp parse_location(nil), do: nil
defp parse_location(location) do
# Handle the new format with "└─" prefix
location =
if String.contains?(location, "└─") do
location
|> String.split("└─")
|> List.last()
|> String.trim()
else
location
end
case String.split(location, ":", parts: 3) do
[path, line_number | _rest] ->
case Integer.parse(String.trim(line_number)) do
{number, _} ->
parts = String.split(path, "/")
filename = List.last(parts)
path =
parts
|> Enum.reverse()
|> tl()
|> Enum.reverse()
|> Enum.join("/")
|> String.trim()
%{
filename: filename,
path: path,
line: number
}
:error ->
nil
end
_ ->
nil
end
end
end