Dialyzer warnings on match spec records and warnings from ignored functions

I have a program that creates a match spec from a local record.

-module(hello).
-export([hello_world/0]).

-record(item, {
    key     :: [binary()]
}).

-spec hello_world() -> ok.
hello_world() ->
    Key = [~"foo", ~"bar"],
    _MatchSpec = [
       {
             #item{key = Key ++ '_', _ = '_'},
             [],
             ['$_']
         }
    ],
    ok.


Running dialyzer on this program generates the following warnings. (with OTP-28):

src/hello.erl                                                                                                                                                   
Line 10 Column 2: Invalid type specification for function hello:hello_world/0. The success typing is () -> none() but the spec is () -> 'ok'                    
Line 11 Column 1: Function hello_world/0 has no local return                    
Line 15 Column 30: Record construction #item{key::nonempty_improper_list(<<_:24>>,'_')} violates the declared type of field key::[binary()]              

The first warning so simply ignoring the function does not help in the long run. Below, I add another function that calls the ignored one.

-spec test() -> ok.
test() ->
  hello_world().

-dialyzer({nowarn_function, hello_world/0}).

Still yields

src/hello.erl
Line 8 Column 2: Invalid type specification for function hello:test/0. The success typing is () -> none() but the spec is () -> 'ok'
Line 9 Column 1: Function test/0 has no local return

I thus have two questions

  1. Is there any way to make Dialyzer compatible with these kind of record constructions?
  2. If not, how can I make Dialyzer ignore warnings that stem from ignored functions?

Thanks,

Ludvig

The key part of the Dialyzer result is this one:

In your record spec it says key :: [binary()], but… when you build the match-spec you assigned [~"foo", ~"bar" | '_'] to it. I’m not an expert on match-specs, so I would guess that’s valid syntax to match any list that starts with those two elements. In other words, it’s valid for match specs but not for the places where you want to validate the actual structure of your record.

This is a fairly well known problem with record specs. The usual solution would go something like this…

%% No types here
-record(item, {key}).

%% The types go here
-opaque item() :: #item{key :: [binary()]}.
-export_type [item/0].

%% And then all functions that work with items have item/0 in their specs…
-spec new() -> item().
new() -> #item{key = key_generator:new()}.

-spec key(item()) -> [binary()].
key(#item{key = Key}) -> Key.
-record(item, {
    key :: maybe_improper_list(binary(), '_')
}).

The above makes dialyzer happy in this case (at least with OTP 27)

nowarn_function pragma doesn’t work as one naively expects. I am not an expert on the Dialyzer internals, but I learned that it only suppresses warnings in a particular function, but not the other functions that call it. It’s important to understand that Dialyzer analyzes the entire call graph as a whole, so one particularly incorrect function or type spec can “infect” multiple functions that call it. So by silencing the warnings generally you just make things harder to troubleshoot. It’s almost never a good idea to use nowarn_function.

The proper way to fix this problem is to address the inconsistency between the item typespec and the hello_world definitions. By writing

-record(item, {
    key     :: [binary()]
}).

you told Dialyzer that correct set of values of field key is a proper list of binaries, i.e. list ending with []. But then the definition of hello_world function contains expression Key ++ '_' , which constructs an improper list [~”foo”, ~”bar”|'_'] (note the difference between this and [~”foo”, ~”bar”, ‘_’], or, more verbosely [~”foo”, ~”bar”, ‘_’ | []]: they are not the same term). So Dialyzer correctly pointed out the error. How to fix this error depends on whether you truly intended to create an improper list or not.

If you intended to create an improper list, see solution by @elbrujohalcon. If not, fix hello_world function like this: key = Key ++ [‘_’]. What I don’t recommend, is trying to mix types necessary for encoding actual values and match specification in one record definition.

This is a known wart in the interaction between match specs and dialyzer.

Match specs are just values (terms) interpreted by some code (e.g. ets) as patterns for sets of values. But they are not patterns in the language itself. Consider

-record(foo, {fie :: integer()}).

and then you say something like #foo{fie = ‘_’}. That’s just a plain value which puts the atom '_’ where you’ve previously said only integer() is allowed. Thus a type error is reported.

I’ve seen two standard solutions for this:

Adjust the record declaration to include the types of the match specs you use:

-record(foo, {fie :: integer() | ‘_’}).

which has the disadvantage of admitting unintended values everywhere.

Or keep the record untyped and create a separate type for non match spec values:

-record(foo,{fie}).
-type foo() :: #foo{fie :: integer()}.

and then you’d use foo() not #foo{} in all function specs.

The second admits stronger type-checking, but has the downside of requiring some code duplication.