Documentation and Static Analysis
Mix.install([
{:kino, github: "livebook-dev/kino", override: true},
{:kino_lab, "~> 0.1.0-dev", github: "jonatanklosko/kino_lab"},
{:vega_lite, "~> 0.1.4"},
{:kino_vega_lite, "~> 0.1.1"},
{:benchee, "~> 0.1"},
{:ecto, "~> 3.7"},
{:math, "~> 0.7.0"},
{:faker, "~> 0.17.0"},
{:utils, path: "#{__DIR__}/../utils"}
])
Navigation
Setup
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
Overview
As developers, we use a combination of documentation, static code analysis, and type specifications to improve the readability of our code, as well as catch possible bugs before compilation.
In this lesson, we’ll see how we can document our projects with ExDoc, ensure consistent code style with Credo, and enforce type specifications with Dialyzer/Dialyxir.
We will be re-using the Math
project created in the previous ExUnit lesson,
so ensure you complete that lesson first.
Documentation
We’ve seen we can use @doc
and @moduledoc
with a multiline string to provide documentation and doc tests.
Doctests automatically find lines containing iex>
in the documentation and ensure the code returns the same value as the line below. Otherwise, the doc test fails.
defmodule Math do
@moduledoc """
Documentation for `Math`.
"""
@doc """
Adds two values.
## Examples
iex> Math.add(2, 2)
4
"""
def add(int1, int2) do
int1 + int2
end
end
Your Turn
We can also read this documentation using the h
helper from the IEx shell.
Open the IEx
shell from the math
project folder, and read the docs for the Math
module.
$ iex -S mix
iex(1)> h Math
Math
Documentation for Math.
iex(2)> h Math.add
def add(int1, int2)
Adds two values.
## Examples
iex> Math.add(2, 2)
4
ExDoc
Alternatively, we can install ExDoc and generate the same documentation seen on HexDocs.
First, add :ex_doc
to the list of dependencies in mix.exs
. The latest version is
on hex.pm. We only need documentation for the :dev
environment, and we do not need it during runtime.
defp deps do
[
{:ex_doc, "~> 0.28", only: :dev, runtime: false},
]
end
Install dependencies.
$ mix deps.get
Then generate documentation for the project.
$ mix docs
This creates a docs/
folder. Inside the docs folder is an index.html
file.
HTML
stands for hyper-text-markup-language. It’s the code used to structure a web page and its content.
You can open .html
files in the browser, so we can open index.html
in the browser to view our documentation.
Open the index.html
file in your browser, and you should see a page similar to the following.
Dialyzer
Dialyzer is a static analysis tool used to provide warnings about your code, such as mismatched types, unreachable code, and other common issues.
To use Dialyzer, we install Dialyxir, which provides conveniences for working with Dialyzer in an Elixir project.
Add :dialyxir
as a dependency to your mix.exs
file in the math
project.
defp deps do
[
{:ex_doc, "~> 0.28", only: :dev, runtime: false},
{:dialyxir, "~> 1.0", only: :dev, runtime: false}
]
end
We can then run Dialyzer by running the following in the command line. Hopefully, there should be no errors.
$ mix dialyzer
...
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m0.82s
Type Specifications
Elixir comes with a notation for declaring types and specifications.
We can use the @spec
module attribute to define the signature of a function.
For example, we could add a @spec
for the math
module’s add/2
function.
@spec
defines the function name, then the types for each argument, then the return value type separated
by the ::
symbol.
defmodule Math do
@spec add(number(), number()) :: number()
def add(int1, int2) do
int1 + int2
end
end
number()
is a built-in type. Here are a few other common types you may find useful.
any()
atom()
map()
tuple()
list()
list(type) # a list where the elements are particular type
float()
integer()
number() # both integers and floats
String.t() # the string type
For a full list of built in types you can see the Basic Types section of the Elixir Typespecs documentation.
This @spec
only considers integers and floats. However, the Math
module should also handle strings, maps, and lists. Note that keyword lists and lists are both covered by the same type.
Currently, if we use Math.add/2
with a non-integer value, we’ll see a warning in the Visual Studio Code editor.
defmodule Math do
@moduledoc """
Documentation for `Math`.
"""
def fail_example do
Math.add([1], [2])
end
@doc """
Adds two values.
## Examples
iex> Math.add(2, 2)
4
"""
def add(int1, int2) do
int1 + int2
end
end
We can run mix dialyzer
to view the full error.
$ mix dialyzer
Total errors: 2, Skipped: 0, Unnecessary Skips: 0
done in 0m0.82s
lib/math.ex:6:no_return
Function fail_example/0 has no local return.
________________________________________________________________________________
lib/math.ex:7:call
The function call will not succeed.
Math.add([1], [2])
will never return since the success typing is:
(number(), number()) :: number()
and the contract is
(number(), number()) :: number()
________________________________________________________________________________
done (warnings were emitted)
Halting VM with exit status 2
Here we see the success typing and the contract spec. The success typing comes from
the return value (int + int
) because the +
operator should work with numbers (integers and floats).
The contract spec is a further restriction we defined. You may not see the success typing warning if you have fully or partially implemented the Math
module.
To resolve this warning, we could add a new type spec for the version of the add/2
function that handles lists.
defmodule Math do
@moduledoc """
Documentation for `Math`.
"""
def fail_example do
Math.add([1], [2])
end
@doc """
Adds two values.
## Examples
iex> Math.add(2, 2)
4
"""
@spec add(list(), list()) :: list()
def add(list1, list2) when is_list(list1) and is_list(list2) do
list1 ++ list2
end
def add(int1, int2) do
int1 + int2
end
end
Now Dialyzer should not print any warnings.
Custom Types
The |
symbol allows for multiple types.
It’s conceptually similar to an operator (||
) but for types.
For example, rather than creating different specifications for the Math.add/2
function,
we could make a single specification that handles both numbers and lists.
@spec add(number() | list(), number() | list()) :: number() | list()
However, this becomes overly verbose if we define the types for integers, floats, strings, maps, lists, and keyword lists.
@spec add(number() | list() | String.t(), number() | list() | String.t()) :: number() | list() | String.t()
Instead, we can use the @type
module attribute to define a custom type.
@type value() :: number() | list() | String.t()
@spec add(value(), value()) :: value()
Your Turn
Add type specifications to the Math
module of your math project.
If you have not already, implement the Math
module code such that
all of your tests from the previous ExUnit lesson pass.
Once complete, mix dialyzer
should be free of errors.
$ mix dialyzer
...
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m0.81s
Credo
Credo is another static analysis tool which focuses on teaching and code consistency. It scans a project’s code for anti-patterns and provides suggestions to improve it’s quality and readability.
Your Turn
Install Credo in your math project by adding it to your dependencies in mix.exs
.
You can find the latest Credo version on hex.pm.
defp deps do
[
{:ex_doc, "~> 0.28", only: :dev, runtime: false},
{:dialyxir, "~> 1.0", only: :dev, runtime: false},
{:credo, "~> 1.6", only: [:dev, :test], runtime: false}
]
end
Ensure you install the new dependency.
$ mix deps.get
Then run the following command to see credo warnings.
$ mix credo
...
Analysis took 0.01 seconds (0.01s to load, 0.00s running 52 checks on 3 files)
3 mods/funs, found no issues.
For example, Credo will warn you if you leave an IO.inspect/2
in your project.
Add IO.inspect/2
anywhere in your project, and run mix credo
to see the error.
$ mix credo
Warnings - please take a look
┃
┃ [W] ↗ There should be no calls to IO.inspect/1.
┃ lib/math.ex:20:5 #(Math.test)
Please report incorrect results: https://github.com/rrrene/credo/issues
Analysis took 0.09 seconds (0.05s to load, 0.04s running 52 checks on 3 files)
4 mods/funs, found 1 warning.
For now, you may find it useful to use credo in future projects for code suggestions that will help you learn to write idiomatic Elixir, beyond this we will not rely on credo heavily throughout this course.
If you would like to go into deeper depth on credo, we recommend you read through the Credo Documentation You can even write your own custom credo checks, or configure additional credo checks to add further rules to your project.
Commit Your Progress
Run the following in your command line from the project folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish documentation and static analysis section"