improve dialyzer

This commit is contained in:
Graham McIntire 2025-10-13 10:02:15 -05:00
parent d75896597e
commit 751b8a5f1b
No known key found for this signature in database
11 changed files with 96 additions and 16 deletions

View file

@ -13,5 +13,14 @@
# IP geolocation plug - these patterns are actually reachable but dialyzer can't prove it
# The remote_ip can be nil or other values beyond just IPv4/IPv6 tuples
~r/lib\/aprsme_web\/plugs\/ip_geolocation\.ex/
~r/lib\/aprsme_web\/plugs\/ip_geolocation\.ex/,
# False positive: Aprs.parse/1 does return {:ok, _} but dialyzer can't see the vendored library types
{"lib/aprsme/is/is.ex"},
# False positive: The nil pattern is handled in the case statement above, dialyzer doesn't track this correctly
{"lib/aprsme/packets.ex"},
# False positive: PacketUtils.get_weather_field can return various types including nil
{"lib/aprsme_web/live/weather_live/callsign_view.ex"}
]

View file

@ -1,3 +1,3 @@
elixir 1.19.0-rc.0-otp-27
elixir 1.19.0-rc.2-otp-27
erlang 27.3.4
gleam 1.11.1

View file

@ -512,7 +512,7 @@ defmodule Aprsme.Is do
attrs = Aprsme.Packet.extract_additional_data(attrs, message)
# Normalize data_type to string if it's an atom
attrs = normalize_data_type(attrs)
attrs = Aprsme.EncodingUtils.normalize_data_type(attrs)
# Submit to GenStage pipeline for batch processing
Aprsme.PacketProducer.submit_packet(attrs)
@ -542,10 +542,6 @@ defmodule Aprsme.Is do
end
end
# Normalize data_type to ensure proper storage
@spec normalize_data_type(map()) :: map()
defp normalize_data_type(attrs), do: Aprsme.EncodingUtils.normalize_data_type(attrs)
@spec update_packet_stats(map(), integer()) :: map()
defp update_packet_stats(stats, current_time) do
new_total = stats.total_packets + 1

View file

@ -11,6 +11,15 @@ defmodule Aprsme.PacketConsumer do
require Logger
@type state :: %{
batch: list(map()),
batch_size: integer(),
batch_timeout: integer(),
max_batch_size: integer(),
timer: reference() | nil
}
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
# Allow unnamed consumers for pool usage
name = opts[:name]
@ -102,6 +111,7 @@ defmodule Aprsme.PacketConsumer do
end
# Helper functions with pattern matching
@spec reset_batch_timer(state()) :: state()
defp reset_batch_timer(%{timer: nil} = state) do
new_timer = Process.send_after(self(), :process_batch, state.batch_timeout)
%{state | batch: [], timer: new_timer}
@ -114,10 +124,12 @@ defmodule Aprsme.PacketConsumer do
end
# Efficient batch length calculation (cached if possible)
@spec batch_length(list()) :: non_neg_integer()
defp batch_length([]), do: 0
defp batch_length(batch), do: length(batch)
# Pattern matching for logging
@spec log_dropped_packets(list(), list()) :: :ok
defp log_dropped_packets([], _), do: :ok
defp log_dropped_packets(dropped, processed) do
@ -150,12 +162,14 @@ defmodule Aprsme.PacketConsumer do
end
# Helper to start batch timer
@spec start_batch_timer(state()) :: state()
defp start_batch_timer(%{batch_timeout: timeout} = state) do
timer = Process.send_after(self(), :process_batch, timeout)
%{state | timer: timer}
end
# Pattern matching for batch utilization check
@spec check_batch_utilization(list(), integer()) :: :ok
defp check_batch_utilization(batch, max_size) when length(batch) > max_size * 0.8 do
Logger.warning("Batch size approaching limit",
batch_status:
@ -169,6 +183,7 @@ defmodule Aprsme.PacketConsumer do
defp check_batch_utilization(_, _), do: :ok
@spec process_batch(list(map())) :: :ok
defp process_batch(packets) do
require Logger
@ -253,6 +268,7 @@ defmodule Aprsme.PacketConsumer do
# )
end
@spec process_chunk(list(map())) :: {non_neg_integer(), non_neg_integer()}
defp process_chunk(packets) do
# Use Stream for memory-efficient packet preparation
packet_stream =

View file

@ -416,7 +416,6 @@ defmodule Aprsme.Packets do
end
end
defp get_canonical_device_identifier(nil), do: nil
defp get_canonical_device_identifier(device_identifier), do: to_string(device_identifier)
@doc """

View file

@ -25,6 +25,11 @@ defmodule AprsmeWeb.AprsSymbol do
background_size: "512px 192px"
}
"""
@spec get_sprite_info(String.t() | nil, String.t() | nil) :: %{
sprite_file: String.t(),
background_position: String.t(),
background_size: String.t()
}
def get_sprite_info(symbol_table, symbol_code) do
# For overlay symbols (A-Z, 0-9), display the base symbol with overlay
if symbol_table && String.match?(symbol_table, ~r/^[A-Z0-9]$/) do
@ -68,6 +73,11 @@ defmodule AprsmeWeb.AprsSymbol do
Gets sprite information for overlay symbols (A-Z, 0-9).
These symbols display the base symbol from the overlay table.
"""
@spec get_overlay_base_symbol_info(String.t()) :: %{
sprite_file: String.t(),
background_position: String.t(),
background_size: String.t()
}
def get_overlay_base_symbol_info(base_symbol_code) do
# Some overlay base symbols are in the alternate table (1), others in overlay table (2)
# Check which table to use based on the symbol code
@ -101,6 +111,7 @@ defmodule AprsmeWeb.AprsSymbol do
Determines which sprite table to use for overlay base symbols.
Some symbols are in the alternate table (1), others in overlay table (2).
"""
@spec get_overlay_base_table_id(String.t()) :: String.t()
def get_overlay_base_table_id(base_symbol_code) do
# Map symbols to the correct sprite table based on APRS specification
# Most overlay symbols are in the alternate table (1)
@ -130,6 +141,11 @@ defmodule AprsmeWeb.AprsSymbol do
Gets sprite information for overlay characters (A-Z, 0-9).
These are rendered from the overlay table.
"""
@spec get_overlay_character_sprite_info(String.t()) :: %{
sprite_file: String.t(),
background_position: String.t(),
background_size: String.t()
}
def get_overlay_character_sprite_info(overlay_char) do
# Use overlay table (table 2) for the overlay character
sprite_file = "/aprs-symbols/aprs-symbols-128-2@2x.png"
@ -171,6 +187,7 @@ defmodule AprsmeWeb.AprsSymbol do
iex> AprsmeWeb.AprsSymbol.normalize_symbol_table("invalid")
"/"
"""
@spec normalize_symbol_table(String.t() | nil) :: String.t()
def normalize_symbol_table(symbol_table) do
cond do
symbol_table in ["/", "\\", "]"] -> symbol_table
@ -193,6 +210,7 @@ defmodule AprsmeWeb.AprsSymbol do
iex> AprsmeWeb.AprsSymbol.normalize_symbol_code("")
">"
"""
@spec normalize_symbol_code(String.t() | nil) :: String.t()
def normalize_symbol_code(symbol_code) do
if symbol_code && symbol_code != "", do: symbol_code, else: ">"
end
@ -211,6 +229,7 @@ defmodule AprsmeWeb.AprsSymbol do
iex> AprsmeWeb.AprsSymbol.get_table_id("]")
"2"
"""
@spec get_table_id(String.t()) :: String.t()
def get_table_id(symbol_table) do
case symbol_table do
# Primary table
@ -233,6 +252,7 @@ defmodule AprsmeWeb.AprsSymbol do
iex> AprsmeWeb.AprsSymbol.render_marker_html("/", "_", "W1AW")
"<div style=\"position: relative; width: 32px; height: 32px; display: flex; align-items: center;\">..."
"""
@spec render_marker_html(String.t() | nil, String.t() | nil, String.t() | nil, integer()) :: String.t()
def render_marker_html(symbol_table, symbol_code, callsign \\ nil, size \\ 32) do
# For symbols without callsigns, use Cachex for better caching
if is_nil(callsign) do
@ -254,6 +274,7 @@ defmodule AprsmeWeb.AprsSymbol do
end
end
@spec generate_marker_html(String.t() | nil, String.t() | nil, String.t() | nil, integer()) :: String.t()
defp generate_marker_html(symbol_table, symbol_code, callsign, size) do
sprite_info = get_sprite_info(symbol_table, symbol_code)
@ -330,6 +351,7 @@ defmodule AprsmeWeb.AprsSymbol do
iex> AprsmeWeb.AprsSymbol.render_style("/", "_", 32)
"width: 32px; height: 32px; background-image: url(/aprs-symbols/aprs-symbols-128-0@2x.png); ..."
"""
@spec render_style(String.t() | nil, String.t() | nil, integer()) :: String.t()
def render_style(symbol_table, symbol_code, size \\ 32) do
sprite_info = get_sprite_info(symbol_table, symbol_code)
@ -347,6 +369,7 @@ defmodule AprsmeWeb.AprsSymbol do
iex> AprsmeWeb.AprsSymbol.extract_from_packet(%{})
{"/", ">"}
"""
@spec extract_from_packet(map()) :: {String.t(), String.t()}
def extract_from_packet(packet) do
symbol_table_id = get_packet_field(packet, :symbol_table_id, "/")
symbol_code = get_packet_field(packet, :symbol_code, ">")
@ -355,6 +378,7 @@ defmodule AprsmeWeb.AprsSymbol do
end
# Helper function to safely extract a value from a packet or data_extended map
@spec get_packet_field(map(), atom(), any()) :: any()
defp get_packet_field(packet, field, default) do
data_extended = Map.get(packet, :data_extended, Map.get(packet, "data_extended", %{})) || %{}

View file

@ -232,9 +232,9 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
value = PacketUtils.get_weather_field(packet, field)
case value do
nil -> false
"N/A" -> false
"" -> false
nil -> false
_ -> true
end
end
@ -246,13 +246,13 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
value = PacketUtils.get_weather_field(packet, key)
case value do
nil ->
nil
"N/A" ->
nil
value when is_number(value) ->
nil ->
nil
value when is_binary(value) ->
case @weather_formatters[key] do
{formatter, separator} ->
{converted_value, unit} = formatter.(value, locale)
@ -262,7 +262,6 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
"#{value}"
end
value when is_binary(value) ->
case @weather_formatters[key] do
{formatter, separator} ->
case Float.parse(value) do
@ -278,8 +277,15 @@ defmodule AprsmeWeb.WeatherLive.CallsignView do
"#{value}"
end
_ ->
nil
value when is_number(value) ->
case @weather_formatters[key] do
{formatter, separator} ->
{converted_value, unit} = formatter.(value, locale)
"#{converted_value}#{separator}#{unit}"
nil ->
"#{value}"
end
end
end
end

View file

@ -5,6 +5,15 @@ defmodule AprsmeWeb.Plugs.RateLimiter do
import Phoenix.Controller
import Plug.Conn
@type key_type :: :ip | :user_agent | (Plug.Conn.t() -> String.t()) | String.t()
@type init_opts :: [
scale: integer(),
limit: integer(),
key: key_type(),
error_message: String.t()
]
@spec init(Keyword.t()) :: init_opts()
def init(opts) do
# Default options
Keyword.merge(
@ -21,6 +30,7 @@ defmodule AprsmeWeb.Plugs.RateLimiter do
)
end
@spec call(Plug.Conn.t(), init_opts()) :: Plug.Conn.t()
def call(conn, opts) do
key = get_key(conn, opts[:key])
scale = opts[:scale]
@ -39,6 +49,7 @@ defmodule AprsmeWeb.Plugs.RateLimiter do
end
end
@spec get_key(Plug.Conn.t(), key_type()) :: String.t()
defp get_key(conn, :ip) do
# Check headers in order of preference
case {get_req_header(conn, "cf-connecting-ip"), get_req_header(conn, "x-forwarded-for"),

View file

@ -23,6 +23,7 @@ defmodule AprsmeWeb.TimeHelpers do
format_time_diff(diff_seconds) <> " " <> gettext("ago")
end
@spec format_time_diff(integer()) :: String.t()
defp format_time_diff(seconds) when seconds < 60, do: gettext("less than a minute")
defp format_time_diff(seconds) when seconds < 120, do: gettext("1 minute")
defp format_time_diff(seconds) when seconds < 3600, do: gettext("%{count} minutes", count: div(seconds, 60))

View file

@ -6,6 +6,7 @@ defmodule AprsmeWeb.TimeUtils do
@doc """
Returns a DateTime that is the specified number of hours before now.
"""
@spec hours_ago(number()) :: DateTime.t()
def hours_ago(hours) when is_number(hours) do
DateTime.add(DateTime.utc_now(), -hours * 3600, :second)
end
@ -13,33 +14,41 @@ defmodule AprsmeWeb.TimeUtils do
@doc """
Returns a DateTime that is one hour before now.
"""
@spec one_hour_ago() :: DateTime.t()
def one_hour_ago, do: hours_ago(1)
@doc """
Returns a DateTime that is 24 hours (one day) before now.
"""
@spec one_day_ago() :: DateTime.t()
def one_day_ago, do: hours_ago(24)
@doc """
Returns a DateTime that is 48 hours (two days) before now.
"""
@spec two_days_ago() :: DateTime.t()
def two_days_ago, do: hours_ago(48)
@doc """
Returns a DateTime that is 7 days (one week) before now.
"""
@spec one_week_ago() :: DateTime.t()
def one_week_ago, do: hours_ago(24 * 7)
@doc """
Returns a DateTime that is the specified number of days before now.
"""
@spec days_ago(number()) :: DateTime.t()
def days_ago(days) when is_number(days) do
DateTime.add(DateTime.utc_now(), -days, :day)
end
@type time_range_atom :: :last_hour | :last_day | :last_two_days | :last_week
@doc """
Returns a tuple of {start_time, end_time} for common time ranges.
"""
@spec time_range(time_range_atom()) :: {DateTime.t(), DateTime.t()}
def time_range(:last_hour), do: {one_hour_ago(), DateTime.utc_now()}
def time_range(:last_day), do: {one_day_ago(), DateTime.utc_now()}
def time_range(:last_two_days), do: {two_days_ago(), DateTime.utc_now()}
@ -48,5 +57,6 @@ defmodule AprsmeWeb.TimeUtils do
@doc """
Returns a default time range of 48 hours.
"""
@spec default_time_range() :: {DateTime.t(), DateTime.t()}
def default_time_range, do: time_range(:last_two_days)
end

View file

@ -40,6 +40,7 @@ defmodule AprsmeWeb.UserAuth do
|> redirect(to: user_return_to || signed_in_path(conn))
end
@spec maybe_write_remember_me_cookie(Plug.Conn.t(), String.t(), map()) :: Plug.Conn.t()
defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end
@ -61,6 +62,7 @@ defmodule AprsmeWeb.UserAuth do
# |> put_session(:preferred_locale, preferred_locale)
# end
#
@spec renew_session(Plug.Conn.t()) :: Plug.Conn.t()
defp renew_session(conn) do
conn
|> configure_session(renew: true)
@ -98,6 +100,7 @@ defmodule AprsmeWeb.UserAuth do
assign(conn, :current_user, user)
end
@spec ensure_user_token(Plug.Conn.t()) :: {String.t() | nil, Plug.Conn.t()}
defp ensure_user_token(conn) do
case get_session(conn, :user_token) do
nil -> ensure_user_token_from_cookie(conn)
@ -105,6 +108,7 @@ defmodule AprsmeWeb.UserAuth do
end
end
@spec ensure_user_token_from_cookie(Plug.Conn.t()) :: {String.t() | nil, Plug.Conn.t()}
defp ensure_user_token_from_cookie(conn) do
conn = fetch_cookies(conn, signed: [@remember_me_cookie])
@ -180,6 +184,7 @@ defmodule AprsmeWeb.UserAuth do
end
end
@spec mount_current_user(map(), Socket.t()) :: Socket.t()
defp mount_current_user(session, socket) do
case session do
%{"user_token" => user_token} ->
@ -229,17 +234,20 @@ defmodule AprsmeWeb.UserAuth do
end
end
@spec put_token_in_session(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
defp put_token_in_session(conn, token) do
conn
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(token)}")
end
@spec maybe_store_return_to(Plug.Conn.t()) :: Plug.Conn.t()
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end
defp maybe_store_return_to(conn), do: conn
@spec signed_in_path(Plug.Conn.t() | Socket.t()) :: String.t()
defp signed_in_path(_conn), do: ~p"/"
end