Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Documentation and Static Analysis

documentation_and_static_analysis.livemd

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

Return Home Report An Issue

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"