Skip to content

Conversation

@Ch4s3
Copy link

@Ch4s3 Ch4s3 commented Oct 29, 2025

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
Copy link
Author

Ch4s3 commented Oct 29, 2025

I'm still working on the tests.

@Ch4s3
Copy link
Author

Ch4s3 commented Oct 29, 2025

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

@oliver-kriska
Copy link

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
Copy link
Author

Ch4s3 commented Oct 30, 2025

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

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
Copy link
Author

Ch4s3 commented Oct 30, 2025

@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
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

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
Copy link
Author

Ch4s3 commented Oct 31, 2025

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
Copy link
Author

Ch4s3 commented Oct 31, 2025

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
Copy link
Author

Ch4s3 commented Oct 31, 2025

@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.

@Ch4s3
Copy link
Author

Ch4s3 commented Nov 3, 2025

I think I found the main problem

@Ch4s3
Copy link
Author

Ch4s3 commented Nov 10, 2025

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

@Ch4s3
Copy link
Author

Ch4s3 commented Nov 20, 2025

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
Copy link
Author

Ch4s3 commented Nov 20, 2025

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

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
Copy link
Author

Ch4s3 commented Nov 21, 2025

@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
Copy link

TD5 commented Nov 24, 2025

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).

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.
Copy link

@TD5 TD5 Nov 24, 2025

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
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.

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.
Copy link

@TD5 TD5 Nov 24, 2025

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).

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

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
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
Copy link

TD5 commented Nov 24, 2025

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
Copy link
Author

Ch4s3 commented Nov 24, 2025

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
Copy link
Author

Ch4s3 commented Nov 25, 2025

@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
Copy link

TD5 commented Nov 26, 2025

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
Copy link
Author

Ch4s3 commented Nov 26, 2025

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.

Comment on lines 195 to 198
cond do
is_list(config[:warning_apps]) -> config[:warning_apps]
resolved == nil -> []
true -> resolved
Copy link
Author

Choose a reason for hiding this comment

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

I'm not crazy about this and may need to change it.

valid_apps
end

defp app_exists?(app) do
Copy link
Author

Choose a reason for hiding this comment

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

this feels a bit defensive but I tripped myself up on this a few times so I added a check.

@Ch4s3
Copy link
Author

Ch4s3 commented Nov 26, 2025

@jeremyjh this should basically work now and I like the api, with the caveat that core_apps feels a bit hacky. I'm putting it down until after US Thanksgiving, but it should be in a reviewable state even though I plan to refactor a bit next week.

end

# Returns core apps configured under :core_apps.
# This is a Dialyxir-only concept used for incremental app mode.
Copy link
Owner

Choose a reason for hiding this comment

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

@Ch4s3 I haven't really had a chance to dig into the details here. Will the new core_apps key only be used for incremental mode or would it apply to normal builds as well? If we want a separate apps list for incremental maybe we should namespace the options so thats more clear.

Copy link
Author

Choose a reason for hiding this comment

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

That's the idea, but I really don't love it. I can definitely namespace it, but I'm not 100% sure yet that its the right options api for this problem. I just couldn't find a reliable way to programatically get these.

Copy link
Owner

Choose a reason for hiding this comment

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

Why do we need a different app list for incremental mode? Could we not use whatever the current options API resolves ?

Copy link
Author

Choose a reason for hiding this comment

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

we could use :apps but I wanted to support apps: :transitive and couln't figure out a good way to automatically pick up things like :erts, and :elixir but I'm afraid it's too clever by half. I'm exploring other options.

Copy link
Author

Choose a reason for hiding this comment

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

without :elixir in apps you end up with missing function warnings for thing like String.t().

Copy link
Author

Choose a reason for hiding this comment

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

I have some new changes shaping up, and I'll publish a test app with this running in CI perhaps later this week.

@Ch4s3
Copy link
Author

Ch4s3 commented Dec 4, 2025

@jeremyjh I have a coworker sanity checking the code, and I'm doing a little doc cleanup and refactoring, but we've confirmed this works locally on our macs, in ci for our large app, and in my test repo on GHA.

@notactuallytreyanastasio

We ran into a REALLY annoying issue that was coming from default low ulimits on Mac OS. I made a PR to @Ch4s3's branch that, hopefully, will make it so no one else loses hours on this wondering why it passes on CI but not on some random Mac lol

Ch4s3#1

@Ch4s3
Copy link
Author

Ch4s3 commented Dec 5, 2025

We ran into a REALLY annoying issue that was coming from default low ulimits on Mac OS. I made a PR to @Ch4s3's branch that, hopefully, will make it so no one else loses hours on this wondering why it passes on CI but not on some random Mac lol

Ch4s3#1

I think this may be best to handle in the readme rather than in code.

…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
Copy link
Author

Ch4s3 commented Dec 5, 2025

@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.

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
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
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
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

Copy link
Author

@Ch4s3 Ch4s3 Dec 11, 2025

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
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.

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
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
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
Copy link
Author

Ch4s3 commented Dec 12, 2025

@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
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
Copy link
Author

Ch4s3 commented Dec 15, 2025

@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
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
Copy link
Author

Ch4s3 commented Dec 15, 2025

@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.

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

6 participants