/
type_match_error.ex
143 lines (114 loc) 路 5.12 KB
/
type_match_error.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
defmodule Hammox.TypeMatchError do
@moduledoc """
An error thrown when Hammox detects that values in a function call don't
match types defined in typespecs.
"""
defexception [:message]
alias Hammox.Utils
@impl true
def exception({:error, reasons}) do
%__MODULE__{
message: "\n" <> message_string(reasons)
}
end
defp human_reason({:arg_type_mismatch, index, value, type}) do
"#{Ordinal.ordinalize(index + 1)} argument value #{inspect(value)} does not match #{Ordinal.ordinalize(index + 1)} parameter's type #{type_to_string(type)}."
end
defp human_reason({:return_type_mismatch, value, type}) do
"Returned value #{inspect(value)} does not match type #{type_to_string(type)}."
end
defp human_reason({:tuple_elem_type_mismatch, index, elem, elem_type}) do
"#{Ordinal.ordinalize(index + 1)} tuple element #{inspect(elem)} does not match #{Ordinal.ordinalize(index + 1)} element type #{type_to_string(elem_type)}."
end
defp human_reason({:elem_type_mismatch, index, elem, elem_type}) do
"Element #{inspect(elem)} at index #{index} does not match element type #{type_to_string(elem_type)}."
end
defp human_reason({:empty_list_type_mismatch, type}) do
"Got an empty list but expected #{type_to_string(type)}."
end
defp human_reason({:proper_list_type_mismatch, type}) do
"Got a proper list but expected #{type_to_string(type)}."
end
defp human_reason({:improper_list_type_mismatch, type}) do
"Got an improper list but expected #{type_to_string(type)}."
end
defp human_reason({:improper_list_terminator_type_mismatch, terminator, terminator_type}) do
"Improper list terminator #{inspect(terminator)} does not match terminator type #{type_to_string(terminator_type)}."
end
defp human_reason({:function_arity_type_mismatch, expected, actual}) do
"Expected function to have arity #{expected} but got #{actual}."
end
defp human_reason({:type_mismatch, value, type}) do
"Value #{inspect(value)} does not match type #{type_to_string(type)}."
end
defp human_reason({:map_key_type_mismatch, key, key_types}) when is_list(key_types) do
"Map key #{inspect(key)} does not match any of the allowed map key types #{key_types |> Enum.map_join(", ", &type_to_string/1)}."
end
defp human_reason({:map_key_type_mismatch, key, key_type}) do
"Map key #{inspect(key)} does not match map key type #{type_to_string(key_type)}."
end
defp human_reason({:map_value_type_mismatch, key, value, value_types})
when is_list(value_types) do
"Map value #{inspect(value)} for key #{inspect(key)} does not match any of the allowed map value types #{value_types |> Enum.map_join(", ", &type_to_string/1)}."
end
defp human_reason({:map_value_type_mismatch, key, value, value_type}) do
"Map value #{inspect(value)} for key #{inspect(key)} does not match map value type #{type_to_string(value_type)}."
end
defp human_reason({:required_field_unfulfilled_map_type_mismatch, entry_type}) do
"Could not find a map entry matching #{type_to_string(entry_type)}."
end
defp human_reason({:struct_name_type_mismatch, nil, expected_struct_name}) do
"Expected the value to be #{Utils.module_to_string(expected_struct_name)}."
end
defp human_reason({:struct_name_type_mismatch, actual_struct_name, expected_struct_name}) do
"Expected the value to be a #{Utils.module_to_string(expected_struct_name)}, got a #{Utils.module_to_string(actual_struct_name)}."
end
defp human_reason({:module_fetch_failure, module_name}) do
"Could not load module #{Utils.module_to_string(module_name)}."
end
defp human_reason({:remote_type_fetch_failure, {module_name, type_name, arity}}) do
"Could not find type #{type_name}/#{arity} in #{Utils.module_to_string(module_name)}."
end
defp human_reason({:protocol_type_mismatch, value, protocol_name}) do
"Value #{inspect(value)} does not implement the #{protocol_name} protocol."
end
defp message_string(reasons) when is_list(reasons) do
reasons
|> Enum.zip(0..length(reasons))
|> Enum.map_join("\n", fn {reason, index} ->
reason
|> human_reason()
|> leftpad(index)
end)
end
defp message_string(reason) when is_tuple(reason) do
message_string([reason])
end
defp leftpad(string, level) do
padding =
for(_ <- 0..level, do: " ")
|> Enum.drop(1)
|> Enum.join()
padding <> string
end
defp type_to_string({:type, _, :map_field_exact, [type1, type2]}) do
"required(#{type_to_string(type1)}) => #{type_to_string(type2)}"
end
defp type_to_string({:type, _, :map_field_assoc, [type1, type2]}) do
"optional(#{type_to_string(type1)}) => #{type_to_string(type2)}"
end
defp type_to_string(type) do
# We really want to access Code.Typespec.typespec_to_quoted/1 here but it's
# private... this hack needs to suffice.
{:foo, type, []}
|> Code.Typespec.type_to_quoted()
|> Macro.to_string()
|> String.split("\n")
|> Enum.map_join(&String.replace(&1, ~r/ +/, " "))
|> String.split(" :: ")
|> case do
[_, type_string] -> type_string
[_, type_name, type_string] -> "#{type_string} (\"#{type_name}\")"
end
end
end