towerops/lib/snmpkit/snmp_lib/mib/error.ex
2026-01-23 12:52:17 -06:00

212 lines
6.2 KiB
Elixir

defmodule SnmpKit.SnmpLib.MIB.Error do
@moduledoc """
Enhanced error handling with recovery and detailed diagnostics.
Provides structured error reporting with context, suggestions, and
precise location information for MIB compilation errors.
"""
@type t :: %__MODULE__{
type: error_type(),
message: binary(),
line: integer() | nil,
column: integer() | nil,
context: map(),
suggestions: [binary()]
}
@type error_type ::
:syntax_error
| :semantic_error
| :import_error
| :type_error
| :constraint_error
| :duplicate_definition
| :file_not_found
| :unterminated_string
| :unexpected_token
| :unexpected_eof
| :invalid_number
| :invalid_identifier
defstruct [:type, :message, :line, :column, :context, suggestions: []]
@doc """
Create a new error with detailed context and suggestions.
## Examples
iex> SnmpKit.SnmpLib.MIB.Error.new(:unexpected_token,
...> expected: :max_access,
...> actual: :access,
...> line: 42,
...> column: 10
...> )
%SnmpKit.SnmpLib.MIB.Error{
type: :unexpected_token,
message: "Expected max_access, but found access",
suggestions: ["Did you mean 'MAX-ACCESS' instead of 'ACCESS'?"]
}
"""
@spec new(error_type(), keyword()) :: t()
def new(type, opts \\ []) do
%__MODULE__{
type: type,
message: generate_message(type, opts),
line: opts[:line],
column: opts[:column],
context: opts[:context] || %{},
suggestions: generate_suggestions(type, opts)
}
end
@doc """
Format an error for display with optional color coding.
## Examples
iex> error = SnmpKit.SnmpLib.MIB.Error.new(:syntax_error, line: 42, column: 10)
iex> SnmpKit.SnmpLib.MIB.Error.format(error)
"Error at line 42, column 10: Syntax error"
"""
@spec format(t(), keyword()) :: binary()
def format(%__MODULE__{} = error, _opts \\ []) do
location = format_location(error)
suggestions = format_suggestions(error.suggestions)
base_message =
case location do
"" ->
"#{error.type |> to_string() |> String.replace("_", " ") |> String.capitalize()}: #{error.message}"
loc ->
"#{loc}: #{error.message}"
end
case suggestions do
"" -> base_message
sugg -> "#{base_message}\n#{sugg}"
end
end
# Generate human-readable error messages
defp generate_message(:unexpected_token, opts) do
expected = format_token_value(opts[:expected])
actual = format_token_value(opts[:actual])
value = opts[:value]
message = opts[:message]
cond do
message != nil -> message
value != nil -> "Expected #{expected}, but found #{actual} '#{value}'"
actual == "" or actual == "nil" -> "Expected #{expected}, but found end of input"
expected == "" or expected == "nil" -> "Unexpected token #{actual}"
true -> "Expected #{expected}, but found #{actual}"
end
end
defp generate_message(:unexpected_eof, opts) do
expected = to_string(opts[:expected])
"Unexpected end of file, expected #{expected}"
end
defp generate_message(:unterminated_string, _opts) do
"Unterminated string literal"
end
defp generate_message(:invalid_number, opts) do
value = opts[:value]
"Invalid number format: '#{value}'"
end
defp generate_message(:invalid_identifier, opts) do
value = opts[:value]
"Invalid identifier: '#{value}'"
end
defp generate_message(:file_not_found, opts) do
file = opts[:file]
"File not found: #{file}"
end
defp generate_message(:import_error, opts) do
symbol = opts[:symbol]
from_module = opts[:from_module]
"Cannot import '#{symbol}' from '#{from_module}'"
end
defp generate_message(:duplicate_definition, opts) do
name = opts[:name]
"Duplicate definition of '#{name}'"
end
defp generate_message(type, _opts) do
type |> to_string() |> String.replace("_", " ") |> String.capitalize()
end
# Helper to format token values properly
defp format_token_value(nil), do: "unknown"
defp format_token_value(""), do: "unknown"
defp format_token_value(value) when is_atom(value), do: to_string(value)
defp format_token_value(value) when is_binary(value), do: value
defp format_token_value(value), do: inspect(value)
# Generate helpful suggestions based on error type and context
defp generate_suggestions(:unexpected_token, opts) do
case {opts[:expected], opts[:actual]} do
{:max_access, :access} ->
["Did you mean 'MAX-ACCESS' instead of 'ACCESS'?"]
{:current, :mandatory} ->
["'mandatory' is deprecated, use 'current' instead"]
{:object_type, :object_identity} ->
["Check if this should be 'OBJECT-TYPE' or 'OBJECT-IDENTITY'"]
_ ->
[]
end
end
defp generate_suggestions(:unterminated_string, _opts) do
["Check for missing closing quote", "Verify string escaping is correct"]
end
defp generate_suggestions(:file_not_found, opts) do
file = opts[:file]
suggestions = ["Check if the file path is correct"]
# Add spelling suggestions if file seems like a common MIB name
if file && (String.contains?(file, "RFC") or String.contains?(file, "MIB")) do
suggestions ++
["Check MIB file naming conventions", "Verify file extension (.txt, .mib, .my)"]
else
suggestions
end
end
defp generate_suggestions(:import_error, _opts) do
[
"Check if the imported MIB is available",
"Verify the symbol name is spelled correctly",
"Ensure import dependencies are in the correct order"
]
end
defp generate_suggestions(_, _opts), do: []
# Format location information
defp format_location(%{line: nil, column: nil}), do: ""
defp format_location(%{line: line, column: nil}), do: "Line #{line}"
defp format_location(%{line: nil, column: col}), do: "Column #{col}"
defp format_location(%{line: line, column: col}), do: "Line #{line}, column #{col}"
# Format suggestions
defp format_suggestions([]), do: ""
defp format_suggestions(suggestions) do
formatted = Enum.map_join(suggestions, "\n", &"#{&1}")
"Suggestions:\n#{formatted}"
end
end