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", line: 42, column: 10, context: %{}, 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) "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