Overriding specs and types from third-party libraries
When using third-party libraries or OTP modules, their type specs may be imprecise, overly broad, or incompatible with eqWAlizer. Rather than using escape hatches throughout your codebase, eqWAlizer supports centralized override modules that replace specs and type aliases at type-checking time.
Two dedicated modules can be used for this purpose:
eqwalizer_specs: overrides function specs from external modules.eqwalizer_types: overrides type aliases from external modules.
These modules act as a single source of truth for spec/type adjustments and affect the entire project, so changes should be made with caution.
Overriding specs with eqwalizer_specs
The eqwalizer_specs module lets you provide replacement specs for functions
from OTP or third-party libraries. eqWAlizer will use the overridden spec
instead of the original one when type-checking call sites.
Syntax
Function names are written as quoted atoms of the form 'module:function'.
Each overridden function must have:
- A
-specattribute with the replacement spec. - A function clause whose body is
error(eqwalizer_specs)(a dummy body that is never executed).
The function clause must have the same arity as the original, with throwaway
arguments (_).
-module(eqwalizer_specs).
-compile([export_all, nowarn_export_all]).
-spec 'lists:keyfind'(Key, N, TupleList) -> Tuple | false when
Key :: dynamic(),
N :: pos_integer(),
TupleList :: [Tuple],
Tuple :: tuple().
'lists:keyfind'(_, _, _) -> error(eqwalizer_specs).
Defining helper types
You can define helper types directly in the eqwalizer_specs module and
reference them in your overridden specs:
-type crypto_cipher_aead() ::
aes_128_gcm
| aes_256_gcm
| chacha20_poly1305.
-spec 'crypto:crypto_one_time_aead'(
Cipher :: crypto_cipher_aead(),
Key :: iodata(),
IV :: iodata(),
InText :: iodata(),
AAD :: iodata(),
EncFlg :: boolean()
) -> binary().
'crypto:crypto_one_time_aead'(_, _, _, _, _, _) -> error(eqwalizer_specs).
Overriding type aliases with eqwalizer_types
The eqwalizer_types module lets you provide replacement definitions for
type aliases exported by OTP or third-party libraries. eqWAlizer will use
the overridden definition wherever the original type is referenced, with some caveats.
Syntax
Type names are written as quoted atoms of the form 'module:type'. Each
overridden type must be:
- Declared with a
-typeattribute. - Listed in an
-export_typeattribute.
-module(eqwalizer_types).
-export_type([
'digraph:vertex'/0,
'io:format'/0,
'ssl:sslsocket'/0
]).
-type 'digraph:vertex'() :: dynamic().
-type 'io:format'() :: string() | binary().
-type 'ssl:sslsocket'() :: dynamic().
Known limitations
Cannot override unexported types
If the original type in the third-party module is not exported, adding an
override in eqwalizer_types and exporting it there will not help — eqWAlizer
will still report a "type exists but is not exported" error referring to the
original module. The override mechanism can only replace types that are already
exported by their defining module.
Mutually recursive type overrides do not work
If two types in the same module reference each other:
%% In foo.erl
-type a() :: {ok, b()}.
-type b() :: {error, a()}.
You cannot override both in eqwalizer_types, because the overrides would need
to cross-reference the originals, creating a cycle that eqWAlizer cannot
resolve:
%% In eqwalizer_types.erl — this does NOT work
-type 'foo:a'() :: {ok, foo:b()}.
-type 'foo:b'() :: {error, foo:a()}.
When to use overrides
- Overly broad return types: an OTP function returns
term()but actually returns a well-known shape that your code depends on. - Missing or incorrect specs: a library function has no spec, or has a spec that is incorrect for your usage.
- Replacing
term()withdynamic(): to get partial type-checking signal instead of silently accepting everything.