- Fix doctests for Accounts, Agents.Stats, Snmp to match actual behavior - Fix dynamic_extra_test vendor post-processing tests to seed sensor data - Fix activity_controller_test to seed devices for feed data - Fix session_manager_test to create browser session for test - Fix topology_test link creation for connection test - Fix device_monitor/driver_worker tests for unique job constraints - Fix accounts_test expired_tokens assertion (magic link token is expired) - Fix happy_path_test and show_events_test to seed monitor data - Fix admin user_live_test user.name -> user.email (no name field) - Fix schema_test to seed activity data - Fix mobile_qr_live_test to match actual template text - Fix SnmpKit.MIB doctests and tests for enriched return values - Fix onboarding_live, mobile_controller, mib_test weak assertions - Remove dead code and fix credo warnings
215 lines
6.3 KiB
Elixir
215 lines
6.3 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",
|
|
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
|