Dialyxir

repository·master·Indexed 23 days ago

https://github.com/jeremyjh/dialyxir

A set of Mix tasks that simplify the use of Dialyzer in Elixir projects. It handles compilation, PLT (Persistent Lookup Table) management, and warning formatting. Key features include the `mix dialyzer` analysis task, `mix dialyzer.explain` for warning details, and support for various output formats including GitHub Actions. It provides configuration options for custom PLT paths, warning flags, and multiple methods for ignoring warnings via module attributes or ignore files.

Tokens
6.6K
Snippets
23
Records
27
Agent score
83%

What's inside Dialyxir

  1. Configure Dialyxir with GitHub Actions

    master

    To optimize Dialyxir runs in GitHub Actions, use a caching strategy for Preloaded Type Lists (PLTs). This prevents re-creating PLTs on every run, which is a time-consuming process.

    Key implementation details:

    1. Cache Key: Use a key that incorporates the runner OS, Erlang/OTP version, Elixir version, and the hash of mix.lock. This ensures the cache is invalidated when dependencies or the runtime change.
    2. Cache Path: Store PLTs in priv/plts.
    3. Separate Save Step: Use actions/cache/save as a distinct step from actions/cache/restore. This ensures that if mix dialyzer fails (which is common when new warnings are introduced), the successfully created PLT cache is still saved for future runs.
    4. Output Formats: When running mix dialyzer, use both --format github and --format dialyxir.
      • --format github: Enables GitHub to display warnings directly in the Pull Request /files annotation view.
      • --format dialyxir: Ensures the raw logs contain the full warning details for easier debugging.
    steps:
      - name: Check out source
        uses: actions/checkout@v2
    
      - name: Set up Elixir
        id: beam
        uses: erlef/setup-beam@v1
        with:
          otp-version: "24.1"
          elixir-version: "1.12.3"
    
      - name: Restore PLT cache
        id: plt_cache
        uses: actions/cache/restore@v3
        with:
          key: |
            plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}
          restore-keys: |
            plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }-}
          path: |
            priv/plts
    
      - name: Create PLTs
        if: steps.plt_cache.outputs.cache-hit != 'true'
        run: mix dialyzer --plt
         
      - name: Save PLT cache
        id: plt_cache_save
        uses: actions/cache/save@v3
        if: steps.plt_cache.outputs.cache-hit != 'true'
        with:
          key: |
            plt-${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-${{ hashFiles('**/mix.lock') }}
          path: |
            priv/plts
    
      - name: Run dialyzer
        run: mix dialyzer --format github --format dialyxir
  2. Install Dialyxir in a Mix project

    master

    To add Dialyxir to your Elixir project, add it to your deps function in mix.exs. It is recommended to restrict it to :dev and :test environments and set runtime: false to avoid including it in production builds.

    After updating mix.exs, run mix deps.get and mix deps.compile to install the dependency.

    defp deps do
      [
        {:dialyxir, "~> 1.4", only: [:dev, :test], runtime: false},
      ]
    end
  3. Ignore Dialyzer warnings

    master

    There are three ways to ignore warnings in Dialyxir:

    1. Module Attribute

    Use the @dialyzer attribute directly in your Elixir module to suppress specific warnings.

    @dialyzer {:nowarn_function, rollback: 1}

    2. Simple String Matches (Legacy)

    Specify a file path via :ignore_warnings in mix.exs. This file contains lines that partially match the output of mix dialyzer --format dialyzer.

    dialyzer: [ignore_warnings: "dialyzer.ignore-warnings"]

    Use a .dialyzer_ignore.exs file (or a custom path via :ignore_warnings). This file is an Elixir list containing tuples or Regexes. These are matched against the short-description format (mix dialyzer --format short).

    To generate entries for your ignore file, use:

    • mix dialyzer --format ignore_file (groups by file/type)
    • mix dialyzer --format ignore_file_strict (more granular, recommended)

    Example .dialyzer_ignore.exs content:

    [
      {file, warning_type},
      {file, warning_description},
      {file},
      ~r/regex_pattern/
    ]
  4. Best practices for Dialyzer in Continuous Integration

    master

    When running Dialyzer in CI, follow these guidelines to optimize build times:

    1. Cache PLT files: Building the project-level PLT can be slow. Use your CI's caching system to store the PLT files.
    2. Custom PLT Paths: Store PLT files in the priv/ directory (e.g., priv/plts/project.plt) instead of the default _build/ location to make them easier to cache.
    3. Ignore PLT files in Git: Ensure your .gitignore includes the PLT files and their hashes so they aren't committed to your repository.
    4. Rebuild on Runtime Changes: Rebuild the PLT whenever you change the Erlang or Elixir versions in your build environment.

    Example .gitignore for PLT files:

    /priv/plts/*.plt
    /priv/plts/*.plt.hash
  5. Configure CircleCI for Dialyxir PLT caching

    master

    To optimize Dialyxir execution in CircleCI, you can cache the Preloaded Terminology (PLT) files. This prevents the expensive --plt creation step from running on every build.

    Follow these steps in your CircleCI configuration:

    1. Version Tracking: Save the current Elixir and Erlang versions to a file (e.g., .elixir_otp_version) to use as part of your cache key. This ensures that if the runtime changes, the cache is invalidated.
    2. Restore Cache: Use a cache key that combines the architecture, the checksum of your version file, and the checksum of mix.lock. This ensures the cache is specific to your dependencies and runtime.
    3. Create PLTs: Run mix dialyzer --plt to generate the PLTs if they are not restored from the cache.
    4. Save Cache: Save the priv/plts directory using the same cache key used for restoration.
    5. Run Dialyzer: Execute mix dialyzer as usual.
    ---
    version: 2
    
    jobs:
      build:
        docker:
          - image: cimg/elixir:1.14
    
        steps:
          - checkout
    
          # Compile steps omitted for simplicity
    
          # Cache key based on Erlang/Elixir version and the mix.lock hash
          - run:
              name: "Save Elixir and Erlang version for PLT caching"
              command: echo "$ELIXIR_VERSION $ERLANG_VERSION" > .elixir_otp_version
    
          - restore_cache:
              name: "Restore PLT cache"
              keys:
                - plt-{{ arch }}-{{ checksum ".elixir_otp_version" }}-{{ checksum "mix.lock" }}
    
          - run:
              name: "Create PLTs"
              command: mix dialyzer --plt
    
          - save_cache:
              name: "Save PLT cache"
              key: plt-{{ arch }}-{{ checksum ".elixir_otp_version" }}-{{ checksum "mix.lock" }}
              paths: "priv/plts"
    
          - run:
              name: "Run dialyzer"
              command: mix dialyzer
  6. Run Dialyzer analysis with `mix dialyzer`

    master

    Run the analysis from your project's root directory. Dialyxir will automatically compile the project and create or update the required PLT (Persistent Lookup Table) files if necessary.

    mix dialyzer
    mix dialyzer
  7. Configure GitLab CI for Dialyxir

    master

    To optimize Dialyxir in GitLab CI, use a multi-stage pipeline that separates compilation from type checking. This allows you to cache dependencies and Precompiled Load Targets (PLTs) to prevent Dialyzer from needing to recompile the project in every job.

    Key Strategies:

    • Caching: Use .tool-versions and mix.lock as cache keys to ensure the cache is specific to your Erlang/Elixir versions and dependency tree.
    • PLT Caching: Cache the priv/plts directory to share precompiled targets between jobs.
    • Cache Policies:
      • Use pull-push for the job that generates the PLT (e.g., dialyzer-plt).
      • Use pull for jobs that only consume the PLT (e.g., dialyzer-check) to avoid unnecessary uploads.
    • Job Dependencies: Use needs to ensure the type-checking stage only runs after the build and PLT generation stages are complete.
    image: elixir:1.14
    
    stages:
      - compile
      - check-elixir-types
    
    build-dev:
      stage: compile
      cache:
        - key:
            files:
              - .tool-versions
              - mix.lock
          paths:
            - deps/
            - _build/dev
          policy: pull-push
      script:
        - mix do deps.get, compile
    
    dialyzer-plt:
      stage: check-elixir-types
      needs:
        - build-dev
      cache:
        - key:
            files:
              - .tool-versions
              - mix.lock
          paths:
            - priv/plts
          policy: pull-push
      script:
        - mix dialyzer --plt
    
    dialyzer-check:
      stage: check-elixir-types
      needs:
        - dialyzer-plt
      cache:
        - key:
            files:
              - .tool-versions
              - mix.lock
          paths:
            - priv/plts
          policy: pull
      script:
        - mix dialyzer --format short
  8. Understand the `:guard_fail` warning

    master

    The :guard_fail warning is triggered when a function guard presents an impossible condition or when the calls made will never succeed against the specified guards. This typically happens when a guard expression is logically impossible (e.g., 0 > 1) or when a function is called with arguments that can never satisfy its guard clauses.

    defmodule Example do
            def ok() when 0 > 1 do
              :ok
            end
          end
  9. Configure PLT (Persistent Lookup Table) settings

    master

    The PLT is a cached output of the Dialyzer analysis used to avoid re-analyzing the standard library and OTP modules every time. By default, Dialyxir builds core Erlang/Elixir files in $MIX_HOME and a project-specific file in _build/$MIX_ENV/.

    You can customize PLT behavior in your mix.exs using the following keys:

    • :plt_core_path: Specify a custom path for the core Erlang/Elixir PLT files (instead of using $MIX_HOME).
    • :plt_local_path: Specify a custom directory for the project-specific PLT file.
    • :plt_file: Specify a custom filename for the project PLT. Note: This is deprecated for local use but useful in CI. To silence deprecation warnings in CI, use plt_file: {:no_warn, "/path/to/file"}.
    • :plt_apps: Specify a list of applications to include in the PLT. Using this replaces the default apps ([:erts, :kernel, :stdlib, :crypto]) and prevents automatic dependency addition.
    • :plt_add_apps: Add specific applications to the default list.
    • :plt_ignore_apps: A list of applications to exclude from the PLT.
  10. Specify custom BEAM search paths

    master

    By default, Dialyxir only searches the ebin directory in your current _build environment. To include additional locations for BEAM files, use the :paths key in your dialyzer configuration.

    def project do
      [...
        dialyzer: [
          paths: ["_build/dev/lib/my_app/ebin", "_build/dev/lib/foo/ebin"]
        ]
      ]
    end
  11. Configure Dialyzer warning flags

    master

    You can enable or disable specific Dialyzer analysis features via the flags key in your dialyzer configuration. Since version 0.4, only :unknown is enabled by default. To enable others, pass them as a list of atoms or strings (using the -W convention).

    def project do
      [...
        dialyzer: [
          flags: ["-Wunmatched_returns", :error_handling, :underspecs]
        ]
      ]
    end
  12. Manage OTP application dependencies in PLT

    master

    By default, Dialyxir transitively adds all OTP application dependencies (as seen in mix app.tree) to your PLT. You can control this behavior using the :plt_add_deps key in your dialyzer configuration:

    • :app_tree: (Default) Includes all transitive OTP runtime application dependencies.
    • :apps_direct: Includes only direct OTP runtime application dependencies, excluding the full transitive tree.

    This is useful for reducing memory usage in large dependency trees.

    def project do
      [...
        dialyzer: [
          plt_add_deps: :apps_direct,
          plt_add_apps: [:wx],
          plt_ignore_apps: [:mnesia]
        ]
      ]
    end