Erlang: find cross-app calls using xref
At work, we use the multi-app project pattern to organize our codebase. This lets us track everything in a single repository but still keep things isolated.
For isolation, we wanted to restrict apps to only be able to call the public interfaces of other apps (similar to facade pattern). However, since everything in Erlang is in a global namespace, nothing prevents code in one app to call the (exported) functions from another app.
Next best solution—detect the above scenario and raise warnings during code review/CI.
Xref to the rescue:
Xref is a cross reference tool that can be used for finding dependencies between functions, modules, applications and releases.
Xref includes some predefined analysis patterns that perform some common tasks like searching for undefined functions, deprecated function calls, unused exported functions, etc.
How it works: when xref server is started and some modules/applications/releases are added for analysis, it builds a Call Graph: a directed graph data structure containing the calls between functions, modules, applications or releases. It also creates an Inter Call Graph which holds information about indirect calls (chain of calls). It exposes a very powerful query language, which can be used to extract any information we want from the above graph data structures.
To demonstrate this, I created a sample multi-app repository: library_sample. There are some cross-app function calls in this code that we want to detect.
This repo is supposed to represent the functionality of a physical Library. It has four apps: library
, library_api
, library_catalog
, and library_inventory
. library_catalog
has metadata about the books in the library, library_inventory
has information about the availability of books, return dates, etc., library_api
has HTTP handlers which call the above, and library
is the main app which brings it all together.
Let’s say we want that library_api
can call library_catalog
and library_inventory
functions, but catalog and inventory cannot call each other directly.
First, we clone the repo and run rebar3 shell:
Then, we start xref and add our build directory for analysis:
Using xref:q/2
for querying the constructed call graph:
This means that there are no direct calls from the library_inventory
application to the library_catalog
application. But, there is a direct call from library_catalog:get_by_id/1
to library_inventory:get_available_copies/1
.
The query E | library_catalog || library_inventory
can be read as:
E
= All Call Graph Edges|
= The subset of calls from any of the vertices. So| library_catalog
creates a subset which contains calls from thelibrary_catalog
app.||
= The subset of calls to any of the vertices. So,|| library_inventory
further creates a subset of the previous subset which contains calls to thelibrary_inventory
app.
To get both direct and indirect calls, closure E
has to be used:
This tells us that there is an indirect direct call from library_catalog:get_by_id/1
to library_inventory:get_all/0
.
The query language is very powerful, and there are more interesting examples in the xref user’s guide.
But
this only runs the required queries manually in Erlang shell. We want
to be able to run it in continuous integration. Luckily, rebar3 comes
with a way to specify custom xref queries to run when running ./rebar3 xref
, and to raise an error if they don’t match against the expected value defined.
Here’s the xref section from my rebar.config
:
This performs the two queries I want and matches them against the the target value of []
. Sample output:
So, now this is ready for automation.
Interactions
Nice :)