Skip to content

feat: add incremental mode support for Dialyzer (OTP 26+)#575

Open
Ch4s3 wants to merge 7 commits into
jeremyjh:masterfrom
Ch4s3:incremental
Open

feat: add incremental mode support for Dialyzer (OTP 26+)#575
Ch4s3 wants to merge 7 commits into
jeremyjh:masterfrom
Ch4s3:incremental

Conversation

@Ch4s3

@Ch4s3 Ch4s3 commented Oct 29, 2025

Copy link
Copy Markdown

PR description

Summary

This PR introduces support for Dialyzer’s incremental mode (OTP 26+) in Dialyxir, including the ability to run incremental analysis based on passed in OTP applications.

The goal is to make Dialyxir work naturally with Dialyzer’s newer incremental workflow while remaining backwards compatible with existing setups.


What is this for

Dialyzer’s incremental mode is designed around:

  • long-lived, incremental PLTs
  • app-based analysis using --apps / --warning_apps
  • dependency-aware re-analysis

Dialyxir historically drives Dialyzer using classic PLTs and a whole app analysis. Incremental mode can significantly improve analysis of smaller changes on large code bases especially in CI environments.


What this PR adds

1. Incremental mode integration

Dialyxir can now pass incremental: true through to Dialyzer, enabling OTP 26+ incremental analysis.

2. Application-based analysis support

New support for Dialyzer’s:

  • --apps
  • --warning_apps

via Dialyxir configuration.

3. Higher-level configuration options

New symbolic configuration values:

dialyzer: [
  incremental: true,
  core_apps: [:erts, :kernel, :stdlib, :crypto, :public_key, :ssl, :elixir, :logger, :mix],
  apps: :transitive,
  warning_apps: :project
]

Supported behaviors:

  • apps: :project → current project / umbrella apps
  • apps: :transitivecore_apps ++ deps ++ project_apps
  • warning_apps: :project → only project apps
  • warning_apps: :all → all resolved apps

This allows large projects to opt into app-based incremental mode without manually maintaining long, fragile app lists.


Implementation overview

  • Adds app-resolution logic in Dialyxir.Project to compute:

    • project apps (umbrella or single)
    • runtime dependency apps (via public Mix project config)
    • user-defined core apps (mostly things from OTP plus elixir itself)
  • Introduces resolution helpers:

    • resolve_apps/1
    • resolve_warning_apps/2
    • resolve_app_args/2
  • Updates the Mix task to:

    • resolve symbolic config into concrete app lists
    • automatically switch Dialyzer into app-mode when appropriate
    • drop :files when running app-mode to avoid mixed invocation types.

Backwards compatibility

This change is opt-in:

  • If incremental mode is not flagged/configured, Dialyxir behaves exactly as before.
  • Existing plt based Dialyzer workflows are unaffected.
  • App-based incremental mode only activates when the new config/flag values are used.

How to use

Example umbrella setup:

dialyzer: [
  incremental: true,
  core_apps: [:erts, :kernel, :stdlib, :crypto, :public_key, :ssl, :elixir, :logger],
  apps: :transitive,
  warning_apps: :project
]

Run:

mix dialyzer --incremental

This enables Dialyzer’s native incremental mode

Preliminary results

Large codebase initial run


our_app
  Dialyzer exited with code 2
  Incremental mode enabled; skipping PLT check step
  Will use PLT file: ops/plts/dialyxir_erlang-28.1.1_elixir-1.19.3_deps-test_incremental.plt
  ignore_warnings: .dialyzer_ignore.exs
  Starting Dialyzer
  [
    analysis_type: :incremental,
    warning_apps: [:monitoring, :api, :another_app, :another_app_2,
     :another_app_3, :another_app_4, ...],
    ...
  ]
  Total errors: 68, Skipped: 65, Unnecessary Skips: 5
  done in 25m3.7s
  Warning: The created anonymous function has no local return.
  Warning: The function call update! will not succeed.
  Warning: Invalid type specification for function changeset.
  done (warnings were emitted)
  Halting VM with exit status 2
Error: Process completed with exit code 1.

Large app 2nd run with error fix


our_app
  Dialyzer exited with code 0
  Incremental mode enabled; skipping PLT check step
  Will use PLT file: ops/plts/dialyxir_erlang-28.1.1_elixir-1.19.3_deps-test_incremental.plt
  ignore_warnings: .dialyzer_ignore.exs
  Starting Dialyzer
  [
    analysis_type: :incremental,
    warning_apps: [:monitoring, :api, :another_app, :another_app_2,
     :another_app_3, :another_app_4, ...],
    ...
  ]
  Total errors: 65, Skipped: 65, Unnecessary Skips: 5
  done in 0m23.13s

You can see that the second run with a +1/-0 diff ran in 23.13s. This application has around 700k lines of elixir code.

Demonstration App

github
ci run

Refs: https://www.erlang.org/doc/apps/dialyzer/dialyzer.html#incremental-mode
Closes: #498

@Ch4s3

Ch4s3 commented Oct 29, 2025

Copy link
Copy Markdown
Author

I'm still working on the tests.

@Ch4s3

Ch4s3 commented Oct 29, 2025

Copy link
Copy Markdown
Author

This is a related issue to a test failure christhekeele/erlex#6

@oliver-kriska

Copy link
Copy Markdown

first of all thanks you are working in it. I think it's important to say in documentation/readme or somewhere that it use _build folder for cache. So when developers use common approach with cache they cache deps and _build folder before dialyzer step/job. But common practice is to use cache versioning based on mix lock file or something like that. In this case it would mean that dialyzer cache will be stored only in case deps are changed. So not big benefits for incremental feature. I think this is important to explain that developers have to check properly their cache logic what they have. I would suggest to introduce new cache which will directly cache folder where new file dialyzer files are stored. Because this new cache has to be updated everytime with some logic. For example you don't want to store new dialyzer's files from some branch same way as you store main branch to avoid usage branch's cache in main. But you want to store branch files due to multiple runnings of ci/cd if it's common for your development flow to have multiple runs of ci/cd per branch. So for example in GitHub Action you can use branch name in cache name with fallback from main branch, for runs from main only cache with main cache. Also I would suggest to use ci/cd runs number which should be incremental every run, because thanks to that it should store cache everytime when it runs, not only when key is changed. But this has to be checked because last time (a few months ago) when I checked GH Action Cache app it had bug/removed feature for flagging cache to be stored everytime when it runs, because we can assume that every run (often) should contains some code change. Is it know exact folder path for cached files?

@Ch4s3

Ch4s3 commented Oct 30, 2025

Copy link
Copy Markdown
Author

I would suggest to introduce new cache which will directly cache folder where new file dialyzer files are stored. Because this new cache has to be updated everytime with some logic. For example you don't want to store new dialyzer's files from some branch same way as you store main branch to avoid usage branch's cache in main. But you want to store branch files due to multiple runnings of ci/cd if it's common for your development flow to have multiple runs of ci/cd per branch. So for example in GitHub Action you can use branch name in cache name with fallback from main branch

I was thinking about this. Is you thought to just document this behavior or to add some mechanism to the library to better facilitate this caching approach?

Also I would suggest to use ci/cd runs number which should be incremental every run

this is on my todo list.

@oliver-kriska

Copy link
Copy Markdown

I think adding good documentation is base. Right now I don't see how library can give some tool or approach for it. Because it depends on own CI/CD configurations how people use cache. Something can be handled by library only in case library is able to set how to store these incremental files. Thanks library can partly allow to configure it for developers or use some common cache so developers will not have to configure new cache or something.

@Ch4s3

Ch4s3 commented Oct 30, 2025

Copy link
Copy Markdown
Author

@jeremyjh do you have any pointers for getting the tests to not time out in CI? They all work for me locally.

@jeremyjh

Copy link
Copy Markdown
Owner

@jeremyjh do you have any pointers for getting the tests to not time out in CI? They all work for me locally.

@Ch4s3 Thanks for working on this!

The problem must be in this branch; I reran master - it passed and test times haven't changed. #578

I don't immediately see the issue; there are tests timing out that aren't really doing much. I think I'd start by commenting all the new tests; if it passes then uncomment just the new output test.

@oliver-kriska

Copy link
Copy Markdown

from documentation:

--incremental - The analysis starts from an existing incremental PLT, or builds one from scratch if one does not exist, and runs the minimal amount of additional analysis to report all issues in the given set of apps. Notably, incremental PLT files are not compatible with "classic" PLT files, and vice versa. The initial incremental PLT will be updated unless an alternative output incremental PLT is given.

it means that file location is same but content is different. I tried your PR and when I use plt_local_path and plt_core_path it use same paths so basically no changes for CI/CD behavior I guess. Or do you have different experience? I think only important part is to remove them before running incremental, because these files are not compatibile with each other.
BTW: I can not confirm that it makes some speed up, it felt same with flag or without flag, with true or false in config.

BTW: You wrote you fixed Fix critical API usage bug in Dialyzer.Runner are you sure you put that change in this module in this PR?

@Ch4s3

Ch4s3 commented Oct 31, 2025

Copy link
Copy Markdown
Author

BTW: You wrote you fixed Fix critical API usage bug in Dialyzer.Runner are you sure you put that change in this module in this PR?

I think that’s leftover from an incremental step in some commits I squashed. I need to rewrite the PR description.

@Ch4s3

Ch4s3 commented Oct 31, 2025

Copy link
Copy Markdown
Author

it means that file location is same but content is different. I tried your PR and when I use plt_local_path and plt_core_path it use same paths so basically no changes for CI/CD behavior I guess. Or do you have different experience? I think only important part is to remove them before running incremental, because these files are not compatibile with each other.

Let me double check that I didn't have some environment config for erlang causing an issue here

BTW: I can not confirm that it makes some speed up, it felt same with flag or without flag, with true or false in config.

I can definitely confirm that on my production code base there's a speedup the measurement I included is from that app. I should clarify it is NOT faster to general the PLT and initial run, it is only faster for subsequent runs.

@Ch4s3

Ch4s3 commented Oct 31, 2025

Copy link
Copy Markdown
Author

@oliver-kriska once I have tests working, I'm going to test this in CI on my app and gather timing info as well as double checking the path information and I'll update docs here.

Comment thread test/mix/tasks/dialyzer_test.exs Outdated
Comment thread test/mix/tasks/dialyzer_test.exs Outdated
@Ch4s3

Ch4s3 commented Nov 3, 2025

Copy link
Copy Markdown
Author

I think I found the main problem

@Ch4s3

Ch4s3 commented Nov 10, 2025

Copy link
Copy Markdown
Author

I'm still working on this, I just had to take a break do to work/life business.

@Ch4s3

Ch4s3 commented Nov 20, 2025

Copy link
Copy Markdown
Author

I think the issue here was trying to test the system halt call on older OTP versions. Since the other code path that calls a halt isn't tested, I'm removing the test that verifies a halt on OTP < 26. Hopefully that's an acceptable tradeoff.

@Ch4s3

Ch4s3 commented Nov 20, 2025

Copy link
Copy Markdown
Author

I'm fiddling with how to correctly use this in CI. It seems like just mix dialyzer --incremental plus any formatting should be run without a discrete --plt step. It doesn't seem like OTP generates an incremental plt if you use both flags.

@michalmuskala

Copy link
Copy Markdown

Yes, with the new incremental mode you don't need a separate PLT step - an incremental run is both generating a new PLT and checking your code - internally it was always the same operation, this just explicitly combines them.

@Ch4s3

Ch4s3 commented Nov 21, 2025

Copy link
Copy Markdown
Author

@michalmuskala thanks for weighing in. Am I understanding correctly that incremental mode doesn't build the core language PLT so you need to also have that if you want to avoid getting errors from missing language functions?

Or should we be using --apps and --warning-apps to deal with this?

@TD5

TD5 commented Nov 24, 2025

Copy link
Copy Markdown

Am I understanding correctly that incremental mode doesn't build the core language PLT so you need to also have that if you want to avoid getting errors from missing language functions?
Or should we be using --apps and --warning-apps to deal with this?

Incremental mode analyses whatever modules you give it, including OTP modules (they don't get any special treatment). That means that you do want to use --apps and --warning-apps to make sure everything is included. For code that you don't own (OTP, libraries made by others), I'd suggest using --apps, so Dialyzer knows about them and can comment on their usage, but won't complain about them directly. Then use --warning-apps for code you actually want issues to be raised for.

Sadly, it's not quite as clean and orthogonal as it sounds, because if there is some sort of discrepancy in your --apps, you won't get a direct error reported for it, but your usages of that code from your --warning-apps might end up generating warnings due to your calls into broken code. As such, it's worth having that in the back of your mind when investigating Dialyzer warnings.

For the mental model for how to use --incremental mode, try to forget your existing knowledge of how Dialyzer works, and instead just list all the code you care about as either --apps or --warnings-apps (including OTP and libraries). You call the same command each time, asking Dialyzer to analyse your--apps and --warnings-apps. The incremental command is given a path to a PLT file, and it will worry about building, changing and updating the cache (PLT) file to minimise the work it needs to use between runs. You don't need to do anything explicitly to manage that lifecycle apart from making sure you point it to a consistent PLT file (and as someone pointed out above, perhaps you want to point to a different one per build configuration/branch if you're often moving between them).

Comment thread README.md Outdated

### Incremental Mode

Dialyxir supports Dialyzer's incremental analysis mode (available in OTP 26+). When enabled, Dialyzer will reuse previous analysis results and only analyze changed modules, significantly speeding up subsequent runs.

@TD5 TD5 Nov 24, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly, in incremental mode, Dialyzer analyses the modules that were changed since the PLT was last used (if there is one), plus the set of modules which Dialyzer thinks might depend on it. This perhaps sounds like a trivial difference, but if you have a core module that is used by a lot of your codebase, changing that will cause all the modules that depend on it to need to be re-analysed too (since you might have fixed/broken them with your change!).

In my experience, this leads to people asking: "I changed this one file but Dialyzer analyses hundreds of files - wasn't incrementality supposed to fix this?", with the answer being that incrementally tries to reduce re-analysing irrelevant files, but files can be relevant because they're in a dependency chain with something that has changed.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This perhaps sounds like a trivial difference

No, this makes perfect sense. I was struggling with how to explain this well, perhaps even to myself.

but files can be relevant because they're in a dependency chain with something that has changed

Right, the classic problem that changing a config file causes a massive recompilation.

Comment thread README.md Outdated

**Analyzing specific applications:**

When using incremental mode, you can specify which applications to analyze using the `apps` and `warning_apps` options. This allows you to analyze entire applications, which can be more efficient for large codebases.

@TD5 TD5 Nov 24, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows you to analyze entire applications, which can be more efficient for large codebases.

Strictly, this is true, but can lead to misleading results if you include modules to be analysed with the modules they (transtively) use function or type definitions from. My suggestion would be to analyse all your code in a repo, including any dependencies it has, in one go. Incrementality itself will do the heavy lifting to soundly work out what code isn't affected by a change, avoiding the risk of the mistake above. Of course, at a certain scale you may really have no choice (e.g. if your codebase is so large the Dialyzer analysis for the entire codebase becomes too big to fit into RAM).

Comment thread README.md Outdated
- If `warning_apps` **is** specified, only those applications will have warnings reported.
- Applications in `apps` but not in `warning_apps` are still analyzed (to provide context for the analysis), but warnings will not be reported for them.

This is useful when you want to include dependencies in the analysis (so Dialyzer can find discrepancies in how you use them), but only see warnings for your own code.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my mind, this is the primary motivation for apps and warning_apps, rather than trying to only analyse a subset of a codebase (since that's so error prone).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are great comments, I think I can do a lot better with the readme now. thanks!

@TD5

TD5 commented Nov 24, 2025

Copy link
Copy Markdown

Hi there 👋 I made Dialyzer's incremental mode. I tried to add a bit of context where I could on this PR. I hope that helps.

@Ch4s3

Ch4s3 commented Nov 24, 2025

Copy link
Copy Markdown
Author

Am I understanding correctly that incremental mode doesn't build the core language PLT so you need to also have that if you want to avoid getting errors from missing language functions?
Or should we be using --apps and --warning-apps to deal with this?

Incremental mode analyses whatever modules you give it, including OTP modules (they don't get any special treatment). That means that you do want to use --apps and --warning-apps to make sure everything is included. For code that you don't own (OTP, libraries made by others), I'd suggest using --apps, so Dialyzer knows about them and can comment on their usage, but won't complain about them directly. Then use --warning-apps for code you actually want issues to be raised for.

This is what I'm figured out after some trial and error and the latest changes are an attempt to make passing those through as straightforward as possible. Thanks for further clarification.

Sadly, it's not quite as clean and orthogonal as it sounds, because if there is some sort of discrepancy in your --apps, you won't get a direct error reported for it, but your usages of that code from your --warning-apps might end up generating warnings due to your calls into broken code. As such, it's worth having that in the back of your mind when investigating Dialyzer warnings.

Makes sense.

For the mental model for how to use --incremental mode, try to forget your existing knowledge of how Dialyzer works, and instead just list all the code you care about as either --apps or --warnings-apps (including OTP and libraries). You call the same command each time, asking Dialyzer to analyse your--apps and --warnings-apps. The incremental command is given a path to a PLT file, and it will worry about building, changing and updating the cache (PLT) file to minimise the work it needs to use between runs. You don't need to do anything explicitly to manage that lifecycle apart from making sure you point it to a consistent PLT file (and as someone pointed out above, perhaps you want to point to a different one per build configuration/branch if you're often moving between them).

Awesome, this is such helpful context! I think I'm pretty close on this, I have the branch working against a production repo and giving me meaningful output. Thanks again, and also for your work on incremental mode.

@Ch4s3 Ch4s3 changed the title feat: add incremental mode support for Dialyzer (OTP 26+) [IN PROGRESS] feat: add incremental mode support for Dialyzer (OTP 26+) Nov 25, 2025
@Ch4s3

Ch4s3 commented Nov 25, 2025

Copy link
Copy Markdown
Author

@TD5 could you weigh in again? I'm now just passing --apps and --warning-apps but not the files in incremental mode and I'm not seeing errors for code with trivial dialyzer errors picked up by classic mode.

Based on this test setup from OTP, and the original commit message I'm assuming --apps and --warning-apps have to overlap. I'm not sure how I missed this before.

@TD5

TD5 commented Nov 26, 2025

Copy link
Copy Markdown

Based on this test setup from OTP, and the original commit message I'm assuming --apps and --warning-apps have to overlap. I'm not sure how I missed this before.

That sounds plausible. Honestly, I wrote the code long enough ago that I can't remember the motivation there.

@Ch4s3

Ch4s3 commented Nov 26, 2025

Copy link
Copy Markdown
Author

That sounds plausible. Honestly, I wrote the code long enough ago that I can't remember the motivation there.

I know the feeling! Thanks for the input.

…r, docs, and CI caching

- Rationale: Dialyzer gained incremental analysis in OTP ([commit 963c7d5](erlang/otp@963c7d5)), but Dialyxir lacked first-class support. Adding it speeds subsequent runs by reusing incremental PLTs and enables app-mode analysis for umbrellas without full rebuilds.
- Usage: new `--incremental` flag/config integrates Dialyzer’s incremental pipeline; `apps` lists expand :transitive (deps + project) but still require explicit OTP apps; `warning_apps` stay project-only, are merged into `apps`, and non-project entries are filtered with warnings to match PR jeremyjh#575 guidance.
- Introduce Dialyxir.AppSelection to centralize CLI/config resolution, normalize atoms/strings, expand flags, filter warning_apps, and build dialyzer args without duplicated task logic.
- Refactor Dialyxir.Project app/warning_app resolution (shared helpers, transitive expansion extraction) and slim Mix.Tasks.Dialyzer arg assembly; remove duplicate normalization code.
- Add targeted tests for the resolver plus existing task/project suites to guard app/warning_app semantics and incremental flows.
- Refresh README and CI guides (GitHub Actions, GitLab, CircleCI) to explain incremental usage, how to cache `priv/plts` (optionally per-branch), and note macOS ulimit tips to avoid EMFILE during MD5 hashing.
@Ch4s3 Ch4s3 changed the title [IN PROGRESS] feat: add incremental mode support for Dialyzer (OTP 26+) feat: add incremental mode support for Dialyzer (OTP 26+) Dec 5, 2025
@Ch4s3

Ch4s3 commented Dec 5, 2025

Copy link
Copy Markdown
Author

@jeremyjh I think this is reviewable now. Below is a sketch of how to read through the changes or as best I could reason about how to approach it.

1) Start with the intent and surface changes

  • Changelog: CHANGELOG.md — confirms incremental mode feature addition and headline behavior.
  • README incremental section: README.md — updated guidance for apps/warning_apps, incremental usage, caching, and macOS ulimit tip.

2) Core entry points (behavioral changes)

3) Centralized app/warning_app resolution

  • New resolver module: lib/dialyxir/app_selection.ex
    • Single place that merges CLI + config, expands flags, normalizes atom/string/charlist, filters non-project warning_apps with warnings, and merges warning_apps into apps.
    • Side-effect scope: only emits warnings via Dialyxir.Output.
  • Project-level helpers: lib/dialyxir/project.ex
    • resolve_apps/1 and resolve_warning_apps/1 unify config resolution; see R307 and R337.
    • fallback_list/3 and resolve_list_value/2 handle list configs and backward compatibility; see R354.
    • expand_transitive_apps/1 extracts :transitive expansion to avoid duplication; see R375.
    • Semantics: apps expands :transitive (deps + project) but still requires explicit OTP apps; warning_apps lists are project-only, merged into apps later; nil → empty list for backward compatibility.

4) Incremental PLT handling

  • PLT file handling: lib/dialyxir/plt.ex
    • Confirm file enumeration and incremental PLT handling are compatible with incremental analysis (no regressions in classic PLTs): see R244 for classic vs incremental PLT file handling.

5) Docs and CI caching

  • CI examples: docs/github_actions.md, docs/gitlab_ci.md, docs/circleci.md
    • Clarified cache keys (OTP/Elixir/mix.lock, optional branch keys) and guidance that incremental artifacts live in priv/plts.
  • MacOS ulimit note: README.md
    • EMFILE avoidance tip for large codebases during MD5 hashing.

6) Tests (behavioural coverage)

  • Resolver unit tests: test/dialyxir/app_selection_test.exs
    • Covers incremental gating, CLI overrides, filtering, and merging.
  • Project helpers: test/dialyxir/project_test.exs
    • Expanded scenarios for apps/warning_apps resolution and :transitive expectations.
  • Task integration: test/mix/tasks/dialyzer_test.exs
    • Ensures flags flow into dialyzer args correctly, including incremental/app-mode paths.
  • Dialyzer runner: test/dialyxir/dialyzer_test.exs
    • Confirms runner behavior in incremental contexts.

7) Fixtures and samples

  • Incremental/app-mode fixtures: under test/fixtures/* (e.g., apps_transitive, warning_apps_project, incremental_*)
    • Check they mirror the intended semantics (OTP apps explicit, warning_apps limited to project).

8) Thing to look out for

  • CLI vs config precedence in AppSelection: CLI should win; warning_apps filtered to project apps only; apps expanded for :transitive.
  • Incremental vs classic: incremental should skip classic PLT build when --incremental; classic flow unchanged when not incremental.
  • Caching guidance: priv/plts is the cache root for both classic and incremental artifacts; keys tied to OTP/Elixir/mix.lock (optional branch suffix).
  • ulimit/EMFILE: documented; no code changes, just awareness for large repos.

Comment thread README.md Outdated

Both `apps` and `warning_apps` accept:
- An explicit list of apps: `[:app1, :app2, ...]`
- The `:transitive` flag – automatically includes all dependencies + project apps

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both :transitive and :project modes are deprecated in favor of :app_tree (the default, equivalent to mix app.tree) and :apps_direct (direct app dependencies). In most cases we only want to include only apps that would be deployed in the application, and not other project dependencies.

#223

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure how I missed this, I'll get it addressed

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in most cases we want to add some_otp_stuff ++ [:app_tree] to :apps

@Ch4s3 Ch4s3 Dec 11, 2025

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my one concern is that :apps_direct includes deps and for the way :warning_apps is used in incremental mode you need just a list of your own apps and not the deps.

I really need to either have another flag here or to just require users to declare their app/apps. It's mostly only an issue for umbrella apps though.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a new key, :apps_project that uses Mix.Project.apps_paths() (if defined) to get top level project applications. It only works in :warning_apps and is documented as such.

Comment thread README.md
When using incremental mode, you can tell Dialyzer which applications to analyse
using the `apps` and `warning_apps` options:

- `apps` – all applications that Dialyzer should know about and analyse upstream of your own code

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this key is only meaningful for incremental mode we need to indicate that in the option name, or nest it e.g. incremental: [apps: [...]].

Where possible - where the semantics are the same - we should use the same config names as https://github.com/jeremyjh/dialyxir?tab=readme-ov-file#dependencies - and let them optionally nested to override incremental mode. This will make adoption easier for people.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'll try nesting it. I looked at using plt_add_deps and plt_add_app but the semantics were a bit different and it made the existing code really hard to follow.

… app_tree/apps_direct

Restructure incremental mode configuration to nest `apps` and `warning_apps` under
`incremental` keyword list, and transition from deprecated `:transitive`/`:project`
flags to `:app_tree`/`:apps_direct` flags.

Configuration Structure:
- Changed from flat `incremental: true, apps: [...], warning_apps: [...]` to nested
  `incremental: [enabled: true, apps: [...], warning_apps: [...]]`
- Removed backward compatibility: no support for boolean `incremental: true` or
  top-level `apps`/`warning_apps` keys (WIP branch)

Flag Transition:
- Replaced `:transitive` with `:app_tree` (transitive dependencies + project apps)
- Replaced `:project` with `:apps_direct` (direct dependencies + project apps)

Dependency Resolution Refactoring:
- Refactored `dep_apps()` and `direct_dep_apps()` to use same traversal mechanism
  as `include_deps` for consistency
- Reuses existing code: `reduce_umbrella_children`, `load_external_deps`,
  `traverse_deps_for_apps`, `load_app`, `app_dep_specs`
- `:app_tree` and `:apps_direct` now automatically include OTP apps declared as
  dependencies (like `:elixir`, `:logger`, `:crypto`, `:public_key`)
- Core OTP apps (`:erts`, `:kernel`, `:stdlib`) must still be explicitly listed

Updated files:
- `lib/dialyxir/project.ex`: Config reading, dependency resolution refactoring
- `lib/dialyxir/app_selection.ex`: Flag handling updates
- `lib/mix/tasks/dialyzer.ex`: Documentation updates
- `README.md`: Configuration examples
- All test fixtures and tests: Updated to new structure and flags
…al mode

- Add :apps_project flag that resolves to project apps (only works in warning_apps)
- Prevent :app_tree and :apps_direct from being used in warning_apps (show warning and return empty list)
- Require apps to be specified in incremental mode (cannot be nil)
- Update documentation to reflect these changes
@Ch4s3

Ch4s3 commented Dec 12, 2025

Copy link
Copy Markdown
Author

@jeremyjh there are some sort of tricky tradeoffs to consider here. The existing code for resolving dependencies is built around the way classic PLTs work and assumes that things like :erts, :elixir, :kernel, and so on are already in the PLT. This means that trying to add those items in automatically through the old code causes runtime warnings about loading deps like the following:

Error loading sasl, dependency list may be incomplete.
 {~c"no such file or directory", ~c"sasl.app"}

I can solve that by resolving deps in a less efficient way in a new code path, or by requiring the end user to declare them explicitly in apps: [...]. Any other approach would have to cut into a lot of older code in a way that feels dicey in terms of broad ecosystem support (to me at least).

Update

I think this is ready for re-review. My compromise here is that users have to declare core language dependencies like [:erts, :kernel, :stdlib, :elixir, :logger]. I don't love it but trying to make it work automatically with existing dialixyr code using an existing flag would require a lot of changes to dependency resolution. You can see it in use here

document required OTP apps for :app_tree/:apps_direct configs
add :elixir/:logger to the umbrella fixture’s :apps list
@Ch4s3 Ch4s3 requested a review from jeremyjh December 12, 2025 20:16
@jeremyjh

Copy link
Copy Markdown
Owner

@Ch4s3 is there an issue with always adding [:erts, :kernel, :stdlib, :crypto, :elixir] to the app list when :app_tree or :apps_direct are used? If a user doesn't want some of those, they could just provide a complete list of the apps they want instead of using either of those methods. I'm not sure that is an edge case that even exists.

@Ch4s3

Ch4s3 commented Dec 15, 2025

Copy link
Copy Markdown
Author

@Ch4s3 is there an issue with always adding [:erts, :kernel, :stdlib, :crypto, :elixir] to the app list when :app_tree or :apps_direct are used? If a user doesn't want some of those, they could just provide a complete list of the apps they want instead of using either of those methods. I'm not sure that is an edge case that even exists.

@jeremyjh if the hard coded value is good with you I can absolutely add it to app_tree. I was just trying to avoid hard coding if I could.

@jeremyjh

Copy link
Copy Markdown
Owner

@jeremyjh if the hard coded value is good with you I can absolutely add it to app_tree. I was just trying to avoid hard coding if I could.

It is already hard coded, those are the default apps that go into the Erlang/Elixir PLTs and are included in the project one.

@Ch4s3

Ch4s3 commented Dec 15, 2025

Copy link
Copy Markdown
Author

@jeremyjh I assumed since it was deprecated that I should do something different. I'll add it into my code path.

If you're you ok using the new apps_project flag for the warning apps then it's read for review.

@Wigny

Wigny commented Feb 20, 2026

Copy link
Copy Markdown

Hey folks, how is this work going? I'm excited to have this ready to use and try it out! Is there something we can do to make that happen?

@Ch4s3

Ch4s3 commented Feb 20, 2026

Copy link
Copy Markdown
Author

@Wigny I'm just waiting for now. I've released an incremental only dialyzer wrapper at https://github.com/Ch4s3/assay that's experimental if you want to try that while you wait for this PR to be merged.

@ypconstante

Copy link
Copy Markdown

I did some testing in a project I'm working on, and got some issues with the new configs.

When I tried to use :app_tree I got an error because of sasl:

:dialyzer.run error: No such file, directory or application: "sasl"

When I tried to use :apps_direct I got an error because of credo:

:dialyzer.run error: No such file, directory or application: "credo"

Dialyzer is running with prod env, and credo is available only on test env.

@Ch4s3

Ch4s3 commented Feb 24, 2026

Copy link
Copy Markdown
Author

I did some testing in a project I'm working on, and got some issues with the new configs.

When I tried to use :app_tree I got an error because of sasl:

:dialyzer.run error: No such file, directory or application: "sasl"

When I tried to use :apps_direct I got an error because of credo:

:dialyzer.run error: No such file, directory or application: "credo"

Dialyzer is running with prod env, and credo is available only on test env.

Interesting, I must not have considered running this with MIX_ENV=prod. Is that how you were running dialyzer before?

@ypconstante

Copy link
Copy Markdown

Yeah, in this project we only run dialyzer with MIX_ENV=prod, and it works with just this setting dialyzer: [plt_add_apps: [:mix, :phoenix_storybook]]

@Ch4s3

Ch4s3 commented Feb 24, 2026

Copy link
Copy Markdown
Author

Yeah, in this project we only run dialyzer with MIX_ENV=prod, and it works with just this setting dialyzer: [plt_add_apps: [:mix, :phoenix_storybook]]

Gotcha, I hadn't considered running in prod, I'm a little surprised it ever worked. This is the line that causes the issue. I'm not sure there's a way to meet @jeremyjh 's desire to make the helpers "just work" and to have incremental mode work in MIX_ENV=prod. Incremental mode in dialyzer doesn't have plt_apps, and I use these helpers like app_tree to sort of fake it.

If you can suggest a clean fix, I'm open.

@oliver-kriska

oliver-kriska commented Mar 13, 2026

Copy link
Copy Markdown

plt_ignore_apps not respected in incremental mode

I tested this PR branch on our production codebase (Elixir 1.19.5, OTP 28) and incremental mode works — great work! The speedup is significant:

Run Mode Time
Classic (existing PLT) Classic 5m 34s
Incremental 1st run Incremental 57m 15s (initial PLT build)
Incremental 2nd run (no changes) Incremental 21s
Incremental 3rd run (1 file changed) Incremental 20s

However, I found an issue with plt_ignore_apps being silently ignored in incremental mode.

Our config:

dialyzer: [
  plt_ignore_apps: [:gpt3_tokenizer, :memoize],
  # ...
  incremental: [
    enabled: true,
    apps: [:elixir, :logger, :crypto, :public_key, :ex_unit, :credo, :tidewave] ++ [:app_tree],
    warning_apps: [:enaia]
  ]
]

What happens:

In classic mode, plt_ignore_apps excludes gpt3_tokenizer from the PLT (it caused an infinite loop during classic PLT building). Since dialyzer doesn't know about the module, calls to Gpt3Tokenizer.token_count/1 produce a "function does not exist" warning, which we suppress via a regex filter in our ignore file.

In incremental mode with :app_tree, the new AppSelection module resolves apps independently and does not subtract plt_ignore_apps. So gpt3_tokenizer gets included, dialyzer sees the function exists, no warning is generated, and our ignore filter becomes unused — which causes list_unused_filters: true to fail CI with exit code 1.

Looking at the code, lib/dialyxir/project.ex:91 applies plt_ignore_apps() in the classic path via cons_apps/0, but lib/dialyxir/app_selection.ex has no reference to plt_ignore_apps at all.

Suggested fix:

AppSelection should read plt_ignore_apps from config and subtract them from the resolved app list, consistent with the classic path. This would make migration from classic → incremental seamless for users who rely on plt_ignore_apps.

In our case the infinite loop turned out to be PLT-specific and didn't occur in incremental mode, but the behavioral inconsistency is still a problem for CI pipelines that use list_unused_filters: true.

fyi: Claude Code generated this comment

@Ch4s3

Ch4s3 commented Mar 17, 2026

Copy link
Copy Markdown
Author

@oliver-kriska I didn't do this initially because it required a lot of file handling. I could possibly do it in the future but I'm waiting on @jeremyjh to take another look at the PR. This has been my approach in the mean time

@epinault

epinault commented May 1, 2026

Copy link
Copy Markdown
Contributor

@jeremyjh any chance we can move this forward? large dialyzer project are slow and while it s harder to justify dialyzer with type safety coming, it would be nice to still support this

@Ch4s3

Ch4s3 commented May 1, 2026

Copy link
Copy Markdown
Author

@epinault I built this to work on a large umbrella and it's used on a few other large apps https://github.com/Ch4s3/assay

@epinault

epinault commented May 1, 2026

Copy link
Copy Markdown
Contributor

Is that a replacement then for dialyxir then?

@Ch4s3

Ch4s3 commented May 1, 2026

Copy link
Copy Markdown
Author

@epinault it is for my use case. It's mostly a dumb wrapper around iplt generation, checking, and a formatter. Its a little rough, because I'm still interested in merging this PR to dialyxir. If you have any issues trying it, file an issue over there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OTP 26: Incremental mode for Dialyzer: --incremental

9 participants